Web 前端性能优化清单。
背景
我们如何才能知道我们在性能方面所处的真正水平,以及我们的性能瓶颈到底是什么呢?它是巨大的 JavaScript 文件、缓慢的 Web 字体传输、繁重的图像、还是缓慢的渲染?是否值得去研究 Tree-Shaking、作用域提升、代码拆分,以及所有奇特的加载模式,包括交叉点观察器做懒加载、服务器推送、客户端提示、HTTP/2、service workers 或者说 edge workers?而且,最重要的是,我们该从哪里开始做性能优化,以及我们应该如何建立长期的关注性能的团队文化?
如何度量
Synthetic Testing:多个用户、多个网络环境、甚至多个地理环境下的多个时间的多次测量,从而得到相对准确的结果。
市面工具
- Page Speed Test
- Lighthouse
RUM - Real User Monitoring 真实用户数据检测:网站插入一段脚本,收集用户浏览器、操作系统、页面加载时间等相关信息
哪些指标是最重要的
- TTFB(Time To First Byte)
- Start Render
- First CPU Idle:第一次 CPU 空闲时间,还有个类似指标即 Time To Interactive(TTI)
作为一个性能报表,你可能更多的想要看到问题在哪而不是大家平均加载速度怎样,因此常见的性能统计表有一下三个方法
- 平均值
- 中位值:排序后,在总样本 1/2 处所在的性能数据
- 第 95 百分位值:排序后,找出第 95% 处的数据,通常这个数据是整个数据集中垫底的数据,既能帮你方法问题所在还能让你体验到用户群中最弱势的用户感受
你对系统某处性能的优化对系统整体的作用取决于该处在系统中的重要性以及你提升了多大。
设定目标
- 响应时间 100ms,每秒 60 帧:为了让交互感觉流畅,页面响应用户的输入动作最好小于100ms。为了达到<100毫秒的响应时间,页面必须每隔<50毫秒将控制权交还给主线程。
- 3G 环境下 FID < 100ms, TTI < 5s, Speed Index < 3s, 关键文件大小 < 170KB (gzip压缩后)
- TTI(Time to Interactive):布局已经稳定、关键的Web字体可见、并且主线程已经空闲下来可以处理用户输入的时间点
- FID(First Input Delay):从用户首次与网站进行交互到浏览器实际上能够响应该交互的时间。
- Speed Index:度量页面内容可视化填充的速度,分数越低越好。速度指数分数是根据视觉填充的速度计算的,但它只是一个计算值。
大纲
前端性能优化大纲
- 资源的合并与压缩
- 图片解编码原理和类型选择
- 浏览器渲染机制
- 懒加载预加载
- 浏览器存储
- 缓存机制
- PWA
- SSR
原理
Web 前端性能优化的关键就是理解浏览器的一个请求从发送到返回都经历了什么。从而得出潜在的优化点
- dns 是否可以通过缓存减少 dns 查询时间
- 网络请求的过程走最近的网络环境
- 相同的静态资源是否可以缓存
- 能否减少 http 请求大小
- 减少 http 请求
- 服务端渲染
始终倾向于自行托管第三方资源,出于安全性考虑,为了避免产生指纹,浏览器已实现了分区缓存,使用公共 CDN 不会自动提高性能。
资源的合并与压缩
目的是减少 http 请求数量和减少请求资源的大小。
- html 压缩
- css 压缩
- js 压缩
- 文件合并
- 开启 gzip(Brotli 更优)
- 不适用自定义字体包或尽量使用优化后的字体包
- 使用 tree-shaking、scope hoisting 和 code-splitting
- 懒加载耗性能的组件
不要觉得压缩下空格、换行、注释等效果不明显,但对于流量大的公司而言,每一点优化都是有意义的,节省巨大的流量费用。
通过 chrome 覆盖率 Coverage 工具排查代码中未使用过的代码并将其删除
合并文件可能存在的问题
- 首屏渲染问题
- 缓存失效问题
因此合并文件更推荐
- 公共库合并
- 不同页面的合并
- 具体情况具体分析
图片相关
图片格式
- JPG:有损压缩,压缩率高,不支持透明
- PNG8/PNG24/PNG32:支持透明
- PNG8:256色+支持透明
- PNG24:2^24色+不支持透明
- PNG32:2^24色+支持透明
- webp:压缩程度更好、支持透明,Safari 不支持,通过 JPG 兜底
- svg:代码内嵌、相对较小、图片样式相对简单
相关方式
- 图片压缩、合适的尺寸
- CSS雪碧图:需要关注文件大小,不要太大
- image inline:base64
- 使用矢量图:减少请求
- 响应式图片:picture 标签(srcset、sizes)
- 渐进加载图片:先加载低质量甚至模糊的图片,然后随着页面继续加载,将它们替换微高质量的完整版本
js与css装载与执行
HTML 渲染过程的特点
- 顺序执行、并发加载
- 可以将 HTML 文件理解为函数的入口,HTML 的词法分析是从上到下顺序执行的
- HTML 中引入的外部资源是并发请求的,但对单个域名会存在并发上限
- 是否阻塞
- CSS head 中阻塞页面的渲染
- CSS 不阻塞外部脚本的加载,但会阻塞js的执行
- 直接引入的 js 阻塞页面的渲染
- js 不阻塞资源的加载
- js 顺序执行,阻塞后续 js 逻辑的执行
- 依赖关系
- 页面渲染依赖 CSS 加载
- js 执行顺序的依赖关系
- js 逻辑对于 dom 节点的依赖关系
- 引入方式
- 直接引入:阻塞
- defer:dom 已经构建完成,且顺序执行
- async:不推荐使用
- 异步动态引入 js
加载和执行的一些优化点
- css 样式表置顶
- 用 link 代替 css @import:新版本浏览器已不存在相关性能问题
- js 脚本置底
- 合理使用 js 的异步加载能力
懒加载与预加载
懒加载:很多时候都是针对图片而言的,尤其是电商网站。
预加载:和懒加载相反,在使用之前提前请求,提升用户体验。
- 使用 img src+display none 的方式
- 使用 js Image 对象
- 使用 XMLHttpRequest,但会存在跨域问题
- 使用 PreloadJS 库
预热连接以加速传输
- dns-prefetch 可以在后台执行 DNS 查找
- preconnect 控制浏览器在后台启动连接握手(DNS, TCP, TLS)
- prefetch 要求浏览器请求一个资源
- preload 预加载资源且不执行
回流和重绘
回流
- 当 render tree 中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,这就称为回流 reflow
- 当页面布局或几何属性改变时就需要回流
重绘:当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color,则称为重绘
触发页面重布局
- 盒子模型相关属性
- 定位属性及浮动
- 改变节点内部文字
新建 DOM 的过程
- 获取 DOM 后分隔为多个图层
- 为每个图层的节点计算样式结果
- 为每个节点生成图形和位置
- 将每个节点绘制填充到图层位图中
- 图层作为纹理上传到 GPU
- 符合多个图层的页面上生成最终屏幕像素
将频繁重绘回流的元素单独作为一个独立图层,那么这个 DOM 元素的重绘回流的影响只会在这个图层之中。开发者工具 Layers 可以查看当前页面所有图层情况,Rendering 查看正在重绘的元素
创建图层的方式
- 3D 或透视变换的 CSS 属性
- video 节点
- canvas 节点
- 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点
- 元素有一个 z-index 较低且包含一个复合层的兄弟元素
常用的手动新建图层方式
- transform
- will-change
实战优化点
- 使用 translate 代替 top
- 使用 opacity 代替 visibility
- 不要一条一跳修改 DOM 的样式,预先定义好 class,然后修改 DOM 的 className
- 把 DOM 离线后修改,比如先 display 为 none,然后在修改 100 次,最后再把它显示出来
- 不要把 DOM 节点的属性值放在一个循环里当成循环里的变量,如 offsetHeight、offsetWidth
- 不要使用 table 布局,可能一个很小的改动会造成整个 table 重新布局
- 动画实现的速度的选择
- 对于动画新建图层
- 启动 GPU 硬件加速
浏览器存储
cookie 需要设置过期时间,大小 4kb 左右,支持设置 httponly 限制 js 读写。关键点:对于静态资源,通常是不需要 cookie 信息的,会造成 cdn 流量损耗。解决办法:cdn 的域名和主站的域名分开。
LocalStorage 大小为 5 mb 左右。SessionStorage 同样大小为 5mb 左右,是会话级别的浏览器存储。
IndexDB
- 用于客户端存储大量结构化数据
- 使用索引来实现对该数据的高性能搜索
pwa
- 可靠性:在没有网络的环境下也能提供基本的页面访问,而不会出现未连接到互联网的页面
- 快速:针对网页渲染及网络数据访问有较好优化
- 融入(Engaging):应用可以被增加到手机桌面,并且有普通应用一样有全屏、推送等特性
service worker
- 一个脚本,浏览器独立于当前网页,将其在后台运行,为实现一些不依赖页面或用户交互的特性打开一扇大门
- 在未来这些特性将包括推送消息、背景后台同步、地理围栏定位
- 推出的第一首要特性:拦截和处理网络请求的能力,包括以编程方式来管理被缓存的响应
- 事件驱动:install、fetch
- 相关 api:postMessage
缓存优化
基于 HTTP 的强缓存和协商缓存。
- 仔细检查 expires、max-age、cache-control 和其他 HTTP 缓存报文头是否已正确设置
- 确保没有发送不必要的报头(例如 x-powered-by,pragma,x-ua-compatible,expires等)
- 确保报文中包含有用的安全和性能相关报文头(如 Content-Security-Policy,X-XSS-Protection,X-Content-Type-Options 等)
SSR
利用服务端渲染优化首屏渲染速度。
客户端预渲染:Gatsby、VuePress。
密集计算
能否将 JavaScript 抽离到 Web Worker。Web Worker 的典型使用场景是预加载数据和渐进式 Web 应用程序,这种方式可以预先加载和存储一些数据,以便后续在需要时使用它。
能否将频繁执行的功能抽离到 WebAssembly。
产品侧
优化过渲染体验
- 在加载资源时,我们可以试着总是走在客户的前面一步,这样虽然后台处理了很多事情,但用户体验仍然迅速
- 为了保持客户的关注,我们可以尝试骨架屏幕,而不是展示加载中的一个指示器
- 添加过渡/动画,并在没有更多优化的情况下欺骗用户体验
PRPL 模式
- 推送关键资源 Push critical resource
- 渲染初始路由 Rendering initial route
- 预缓存剩余路由 Pre-caching remaining routes
- 按需延迟加载剩余路由 Lazy-loading remaining routes on demand
App Shell 模式:支持展示用户界面的最小 HTML、CSS 和 JavaScript
API 优化
GraphQL 是 API的一种查询语言,并且是服务器端运行时的,用于通过使用为数据定义的类型系统执行查询。与 REST 不同,GraphQL 可以在单个请求中检索所有数据,并且只响应所需的内容,而不会像 REST 通常那样过度或不足地获取数据。
HTTP2
网络与 http/2
- 启用 OCSP stapling 了吗:通过在服务器上启用 OCSP stapling,可以加快 TLS 握手的速度
- 适配 IPv6 了吗
- 确保所有资源都在 HTTP/2 上运行
- 正确地部署 HTTP/2
- 您的服务器和 CDN 支持 HTTP/2 吗
- 您的服务器和 CDN 是否支持基于 QUIC 的 HTTP(HTTP/3)
- 正在使用 HPACK 压缩吗