干货 | 提升50分,Trip.com 机票基于 PageSpeed 的前端性能优化实践
作者简介
Patrick,携程资深前端开发工程师,专注于前端工程化和性能优化。
前言
网站性能对于用户体验、转化率和流失率、SEO 排名等至关重要,Trip.com 主要用户来自海外,对网站访问性能有更高的要求。能够快速响应的网站通常有机会获取更多流量,并为用户带来更好的体验。
近期我们对 Trip.com 机票站点做了一版性能优化,通过对主要 landing 页面进行系统优化,将页面的 PageSpeed 评分从原本 30 左右提升到 80 分以上。
这里分享在优化过程中的一些经验,将从性能指标、性能测量与优化实践方案三个方面展开,期望可以给大家提供一些思路和参考。
一、性能指标
1.1 性能指标的发展与演进
针对线上项目做性能优化,首先需要有一个确定的可量化的评判标准,用来判断优化工作是否有效。
1.1.1 传统的性能指标以及它们存在的问题
传统的性能指标最典型的是 DOM Ready 时间和页面加载时间(load time):前者指的是初始 HTML 文档完全加载和解析完成的时间,一般是通过 DOMContentLoaded 事件获得;后者指的是整个页面所需的资源(包括脚本、样式、图片等)加载完成的时间,通过全局的 load 事件获取。
普遍存在的问题是:在早先前后端耦合的时代,通过在服务端使用模板引擎渲染出 HTML,能比较好地反映网站性能。后来前端领域的迅猛发展,尤其是随着客户端渲染方案的盛行,以及各种动态技术的大量运用,这两个指标差不多已经失去其原有的意义,无法准确反映性能。
1.1.2 指标和用户实际感受之间的差异
再往后,采用浏览器提供的 Navigation Timing API ,通过 performance.timing 获取从页面开始加载到结束全过程中不同阶段的时间点。用这种方法,开发者可从多个维度去定义一些指标,通过简单的差值计算得到数据,并以此去监控站点性能。
比如在携程现有的UBT(User Behavior Tracking)中基于 Timing API 主要定义了以下 7 个关键指标 DNS、Connect、Request、Response、Blank、Domready 和 Onload。
DNS (domainLookupEnd - domainLookupStart) Connect (connectEnd - connectStart) Request (responseStart - requestStart) Response (responseEnd - responseStart) Blank (domInteractive - responseStart) Domready (domContentLoadedEventEnd - navigationStart) Onload (loadEventEnd - navigationStart)
1.1.3 以用户为中心的性能指标
上述这些指标更侧重于技术角度,跟用户在实际使用过程中的真实感受会有偏差。以此为标准去做性能优化的话,很可能面临的一种场景是,已经把某些特定指标如加载时间的数值大幅减少,但用户体验仍然不佳。基于此,Chrome 团队和 W3C 性能工作组推出了一组 以用户为中心的性能指标,从用户角度更好地去评判页面性能。
这些指标主要包含:
FCP,First Contentful Paint 首次内容绘制 LCP,Largest Contentful Paint 最大内容绘制 TTI,Time to Interactive 可交互时间 TBT,Total Blocking Time 总阻塞时间 CLS,Cumulative Layout Shift 累积布局偏移
1.2 指标介绍
接下来简单介绍下主要性能指标的具体定义:
1.2.1 FCP
FCP 指标测量的是页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。“内容”可以是文本、图像(包括背景图像)、<svg> 元素或非白色的 <canvas> 元素。
这个指标回答了一个用户问题,应用正在运行吗。
还有一个从名称上很接近的指标,FP (首次绘制),它们之间的区别如下:
FP first-paint 大致可以认为是白屏时间 FCP first-contentful-paint 大致可以认为是首屏时间
1.2.2 LCP
这个指标对应的关键用户问题是,内容是否有用,即页面是否已经呈现出对用户有用的内容。
早先有过一些类似的指标比如 FMP (首次有效绘制),但有效绘制的定义是什么通常很难解释,而且算法容易出错。
相反,最大内容绘制的定义简单明了,这里的“内容”和 FCP 中的定义基本一致,指的是在可视区域内的最大图片或文本块完成渲染的时间。
元素大小指的是内容占据的面积大小,即 size = width * height ,不包含边距边框。
大多数情况下,页面上最吸引用户的内容往往就是最大元素,可以视为页面中最重要的内容。
1.2.3 TTI
可交互时间,对应的用户关注点是可以使用吗。
早期,关于可交互时间一直并没有一个清晰明确的定义。刀耕火种的时代,开发者通过自定义时间节点,并在代码中埋点来获取相关数据。
比如通过在 setTimeout 中放一个任务获取执行时间点,再计算到页面开始加载的差值。
setTimeout(function() {
tti = new Date() - navigationStartTime
}, 0)
或者,在使用 React 等特定框架时,通过向主要组件的生命周期函数 componentDidMount 埋点,并以此计算 TTI 时间。
而在 Lighthouse 中,可交互时间指标有了更通用、标准化的定义。TTI 应从 FCP 时间点开始沿时间轴查找,如果出现 5 秒的静默窗口(没有长任务并且不超过 2 个正在处理的 GET 请求),那么最后一个长任务执行结束的时间点即为可交互时间。
长任务指的是执行时间超过 50 ms 的任务。
定义的根据是,主线程上若不存在导致阻塞状态的长任务,则视为此时已可以响应用户交互。
1.2.4 TBT
1.2.5 CLS
1.2.6 其他
SI,Speed Index 速度指数,属于 Lighthouse 六大性能指标之一,使用 speedline 模块来衡量视觉进度
TTFB,Time to First Byte 首字节时间,用于衡量服务器响应能力,所有请求包括页面、 脚本 和 AJAX 等都可以统计
FID,First Input Delay 首次输入延迟,由于主线程繁忙导致用户首次输入的延迟时间
FCI,First CPU Idle 首次 CPU 空闲,与 TTI 指标相似,目前已不推荐使用
二、性能测量
2.1 实验室测量
2.2 现场测量
2.3 定量评估的问题与方案
开发模式启动站点应用与生产模式差别较大,将应用发布到独立测试服务器再进行性能测量
本地启动 Lighthouse 进行测量,在不同时间的系统状态差异较大,部署测量工具到特定服务器
由于环境影响单次测量的差异可能很大,基于 lighthouse NPM 包一次性跑 10 次,去除最大值和最小值之后再取中位数和平均值作为参考
性能分数由 6 大性能指标计算而来,单项指标的数值优化最终在分数上体现可能没有差异,分开对比具体指标数值
三、性能优化方案
3.1 减小包体积
不再使用的冗余代码 复制粘贴的重复代码 非必要的大体积类库 未经优化的图片文件
3.1.1 冗余代码的优化
3.1.2 重复代码的优化
3.1.3 类库开销的优化
3.1.4 图片文件的优化
3.1.5 更激进的做法
3.2 优化资源加载
3.2.1 Resource Hints
Preload 预加载最重要的资源 Render 尽快渲染初始内容 Pre-cache 预缓存其他资源 Lazy load 懒加载其他路由和非关键资源
preconnect 启动早期连接,包括 DNS 查找,TCP 握手等
preload 预加载资源并缓存,以便需要时立即使用
prefetch 预获取资源,优先级比 preload 低,浏览器自行判断合理时间执行操作
不要无限制的滥用,因为使用本身会消耗资源,尤其是添加了但却未使用
资源设置 crossorigin ,对应预处理提示也要设置,否则两者不匹配导致重复加载
3.2.2 Service Worker
3.2.3 优化加载第三方脚本
3.3 组件懒加载
import React from 'react';
import loadable from '@loadable/component';
import LazyLoad from 'react-lazyload';
const LazyComponent = loadable(() => import(/* webpackChunkName: "home_lazy" */ './LazyComponent'));
export function HomePage() {
return (
<>
<MainComponent />
<LazyLoad>
<LazyComponent />
</LazyLoad>
</>
);)
}
懒加载可能导致懒加载组件自身体验下降,可对用户比较频繁使用的组件进行预加载。
过度拆分可能会产生很多体积很小的包,可适当地进行合并。借助 webpack magic comment ,配置相同 chunk name 即可合并打包。
import loadable from '@loadable/component';
export const SortLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './SortLayer'));
export const StopLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './StopLayer'));
export const TimeLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './TimeLayer'));
3.4 优化渲染方式
3.4.1 服务端渲染
将渲染(这里是指 JavaScript 执行层面的)工作转移到服务端,毕竟服务端相对更可控
在首屏之前避免减少资源网络传输,从而减少耗时,网络是更不可控的一个因素
3.4.2 预渲染
3.5 优化长任务
requestIdleCallback API Web Worker 记忆函数 Debounce 和 Throttle
3.5.1 requestIdleCallback API
3.5.2 Web Worker
3.5.3 记忆函数
3.5.4 Debounce 和 Throttle
3.6 React 性能优化
使用 PureComponent 和 Memo 避免不必要的重新渲染,复杂场景适当使用 shouldComponentUpdate 或是 areEqual 方法
函数组件可使用 useMemo 用于记忆计算结果,其他场景可引入外部库如 reselect 简化处理
在更新 state 深层嵌套数据时避免使用深拷贝,可借助 immer 这类库来处理不可变数据
保持 DOM 结构简洁,避免层级过深。比如,最简单的一个点是使用 React.Fragment
3.7 减少布局偏移
为动态元素预留静态空间 图片宽高尺寸固定
四、总结反思
团队招聘信息
我们是携程机票研发团队,负责携程APP/PC端机票业务开发及创新。机票研发在搜索引擎、数据库、深度学习、高并发等方向持续不断地深入探索,持续优化用户体验,提高效率。
在机票研发,你可以和众多技术顶尖大牛一起,真实的让亿万用户享受你的产品和代码,提升全球旅行者的出行体验和幸福指数。
如果你热爱技术,并渴望不断成长,携程机票研发团队期待与你一起腾飞。目前我们前端/后台/数据/测试开发等领域均有开放职位。
简历投递邮箱:tech@ctrip.com,邮件标题:【姓名】-【携程机票】-【投递职位】。
【推荐阅读】
“携程技术”公众号
分享,交流,成长