优化的目的是展示更快、交互响应快、页面无卡顿情况。做优化需要理解浏览器加载和渲染的本质,可以参考 浏览器进程 和 认识优化渲染性能的本质。
包括7个类别35条军规:
首先,与用户感知性能相关有以下类型:
Google和W3C性能工作组提供了几种性能指标有:
其他指标:
Google定义了3个最核心的指标——Core Web Vitals:
对应的,Google官方库 web-vitals,可以在线上或本地测量这3个指标。
LCP可能被这四个因素影响:
FID可能被这四个因素影响:
CLS 能被这三个因素影响:
更多详细优化请参考 web.dev
Google开发的这些工具都支持Core Web Vitals的测量:web-vitals 。
结合这些工具做性能优化:
做性能优化时,会通过各种埋点,来收集用户数据,进行性能分析,简单来说是事后监控。同样的,也应该考虑事前监控,否则,每次发布需求后,去线上看数据是否下降或异常,性能优化永远作为"追赶者/弥补者"的角色在查问题、做优化。那么如何做事前监控——建立流水线机制:
PageSpeed Insights API 或 Lighthouse CI :把 Lighthouse 或 PageSpeed Insights API 集成到 CI 流水线中,输出报告分析。
Playwright 或Puppeteer: Puppeteer和Playwright底层都是基于 Chrome DevTools Protocol ,使用 e2e 自动化测试工具集成到流水线模拟用户操作,得到Chrome Trace Files,也就是平常录制 Performance 后,点击左上角下载的文件。。
Chrome Trace Files:根据谷歌提出的规则分析Trace文件,可以得到每个函数执行的时间。如果函数执行时间超过了一个临界值,可以抛出异常。如果一个函数每次的执行时间都超过了临界值,那么就值得注意了。但是还有一点需要思考的是:函数执行的时间是否超过临界值固然重要,但更重要的是这是不是用户的输入响应函数,与用户体验是否有关。
代理使用:whistle
本地环境、测试环境模拟:nginx
数据上报:TAM,RUM
前端代码打包分析:webpack-bundle-analyzer
首先使用本地代码分析问题,然后本地模拟线上环境验证优化效果,最后再部署到测试环境进行验证。
1. 页面加载时间
2. 加载后响应时间——FID;
3. 视觉稳定性——CLS。
页面进度条在首屏展示后还在持续 loading,持续时间长达 10s 左右,比较影响了用户体验。而进度条的加载时长和 onload 时间密切相关,所以需要减少 onload 时长。结合实际情况,我们使用 ChromeDevTool 作为性能分析工具来观察页面性能情况。
通常进行网络分析需要禁用缓存、启用网络限速(4g/3g) 模拟移动端弱网情况下的加载情况,因为 wifi 网络可能会抹平性能差距。可以发现onload事件被大量媒体资源阻塞了。
网络限速(Fast 3g)条件下,DCL 时长达到 9s 多,L 时长更是到了 20s 以上:
DOM的解析受JS加载和执行的影响,找到最长请求路径文件的耗时,尽量对JS进行压缩、拆分处理(HTTP2下),能减少 DOMContentLoaded 时间。图片、视频、iFrame等资源,会阻塞 onload 事件的触发,需要优化资源的加载时机,尽快触发onload。
使用 Performance 模拟移动端注意手机处理器能力比 PC 差,因此一般将 CPU 设置为 4x slowdown 或 6x slowdown 进行模拟。
观察 Web Vitals ( FP / FCP / LCP / Layout Shift ) 核心页面指标 和 Timings 时长,发现 LCP、DCL和 Onload Event 时间较长,且出现了多次 Layout Shift(需要在 chrome console 左侧 more tools 中 Rendeing 中开启 Layout Shift Regions,在结果中的 Experience 行点击 Layout Shift ,下面的Summary 面板找到具体的偏移内容)。因此,需要使得 LCP 尽量早触发,应该减少页面大块元素的渲染时间,还有就是需要查看优化页面上存在的 Main Long Tasks 长任务数量和时长,选择在开发环境进行录制,如此可以在 Main Timeline 能看到具体的代码执行文件和消耗时长。
使用 ChromeDevTool 内置 lighthouse 对页面进行跑分,发现分数比较低,TTI,SI,TBT,LCP 等不合格,CLS 达到了良好的标准。lighthouse 的评分内容是根据项目整体加载进行打分的,审查出的问题同样包含 Network、Performance 的内容,会提供一些优化建议( Oppotunities 和 Diagnostics 项),如图片大小、移除无用JS等。
首先,根据是否参与首屏渲染将影响 DOM 解析的 JS 资源划分为:
使用 webpack-bundle-analyzer 进行打包分析,发现关键 JS 文件数量多,总体积大,最大文件比较大。
首先,对 Splitchunks 进行正确配置:不能简单的依靠 miniChunks 规则(比如最大公共文件 base.js 的 miniChunks = 3,会导致引用超过3次的模块就被打入该 JS)对页面依赖模块进行抽离打包,要根据具体情况拆分公共依赖。根据业务具体的需求,提取不同页面和组件都有的共同依赖(比如 utils/log/api)到 base.js中:
base: {
name: 'base',
priority: 50,
minChunks: 1,
reuseExistingChunk: true,
},
而其他未指定的公共依赖,新增一个 common.js,将阈值调高到 20 或更高(当前页面数76),让公共依赖成为大多数页面的依赖,提高依赖缓存利用率,两个文件加起来相比优化前体积减少了 50%(80kb)。
其次,对公共组件进行按需加载:分析发现,对于使用 require 来加载 svg 图片,会导致 webpack 将 require 文件夹内的内容一并打包,导致页面图标组件冗余,可以通过配置 babel 的依赖加载路径调整 Icon 的引入方式,通过 import { Fire,ToTop } from 'Icons' 进行按需引入。如此之后,相比优化前体积减少 60%(54kb)。
最后,对业务组件进行代码分割(code splitting):考虑对不在首屏的页面组件进行拆分再延迟加载,减少业务代码 JS 大小和执行时长,可以使用react-loadable、@loadable/component 等库实现,也可以使用React 官方提供的React.lazy。不过,对页面组件进行代码分割会导致某些组件会有渲染的延迟,使用时应该综合用户体验和性能再做决定。
因为有使用到 TreeShaking 优化:对于没用到的包/模块/方法等,treeShaking 检查时会进行删除。可以给引入的包/模块(不是用来做 polyfill 或 shim 之类) 标记为 sideEffects: false
,只要它没有被引用到,整个模块/包都会被完整的移除。
优化前后效果对比:
总体积 | 最大文件体积 | |
优化前 | 528kb | 215kb |
优化后 | 351kb | 137kb |
优化效果 | 33% | 36% |
如果在弱网情况,非关键 JS 可能会成为影响 DOM 解析的因素。
对于可能无效的缓存组件(需要特定的操作才会触发)这类的非关键 JS 资源,可以使用 Resource Hints,针对资源做 Prefetch 处理:检测浏览器是否支持 prefech,支持的情况下我们可以创建 Prefetch 链接,不支持就使用旧逻辑直接加载,这样能更大程度保证页面性能,为下一个页面提供提前加载的支持:
const getPrefetchSupported = () => {
const link = document.createElement('link');
const { relList } = link;
if (!relList || !relList.supports) {
return [false, link];
}
return [relList.supports('prefetch'), link];
};
const prefetch = (url) => {
const [isPrefetchSupport, link] = getPrefetchSupported();
if (isPrefetchSupport) {
link.rel = 'prefetch';
link.as = type;
link.href = url;
document.head.appendChild(link);
} else if (type === 'script') {
// load script
}
};
对于其他比如监控上报等非关键JS资源,可以选择延迟加载它,或者在其他 JavaScript 之后立即加载,或者直到需要时才加载。
对于图片,视频等媒体资源的优化参考:前端图片优化,要使其不阻塞 onload,因此需要进行懒加载处理,而且需要注意懒加载不能阻塞业务的正常展示,应该做好超时处理、重试等兜底措施
iframe 异步加载:iframe 是会阻塞onload 的 触发的,可以将将 iframe 的时机放在 onload 之后,并使用setTimeout触发异步加载iframe,可避免iframe带来的loading影响
字体文件压缩:可能会包含很多设计指定渲染的字体,当字体文件比较大的时候,也会影响到页面的加载和渲染,可以使用 fontmin 将字体资源进行压缩生成精简版的字体文件,压缩后体积减少字体文件30%。
埋点上报优化:图片请求若耗时长就会阻塞页面 onload 事件的触发,解决方案有三种:1. 延迟合并上报;2. 使用 Beacon API ;3. 使用 post 上报。可以采用延迟合并上报的方案,更多可以参考数据上报方式梳理进行优化
通过日志打点、查看 Nginx Accesslog 日志、网关监控耗时,得出以下数据:
发现页面TTFB时间过长的根本原因是:NGW 网关部署和 反向代理网关 Nginx 集群、SSR 服务器程序不在同一区域,导致网络时延的产生。解决方案是让 NGW网关、反向代理网关 Nignx 集群和 SSR 服务器服务机房部署在同一区域,即执行对网关 NGW 进行扩容和分布式服务开启就近访问。
优化前后对比:
30 天网关平均耗时 | |
---|---|
优化前 | 166 ms |
优化后 | 41 ms 优化 75%(125ms) |
虽然,CSS文件的加载不会阻塞页面解析,但会阻塞页面渲染。如果 CSS 文件较大或弱网情况,会影响到页面渲染时间,影响用户体验。通过利用 ChromeDevTool 的 More Tools 里的 Coverage 工具,录制页面渲染时 CSS 的使用率,发现首屏的 CSS 使用率只有 20%,因此考虑对页面首屏的关键CSS进行内联(利用 webpack 插件 critters 实现),与加载其余完整 CSS 文件进行分离,让首屏页面渲染不被 CSS 阻塞,这样当CSS 资源正在下载时,页面就已经能开始正常渲染显示了。
尽量使得首屏页面内容相对固定, 页面元素出现无突兀感,避免图标缺失、背景图缺失、字体大小改变导致页面抖动或者出现非预期页面元素导致页面抖动,采用解决方案如下:
优化前后Lighthouse 跑分对比(提升1倍以上)
性能得分 | |
---|---|
优化前 | 平均 40~ 45 |
优化后 | 平均 80 ~ 85 |
优化前后 LCP与 onload 耗时
FCP 耗时 | onload 耗时 | |
---|---|---|
优化前 | 3.5 s | 6.2s |
优化后 | 1.3 s | 2.5s |