总结现代 Web 优化,无废话。

本文列出的建议并非绝对。具体页面具体分析,不断改进测试,才是正确途径。

关键指标

  • 首次有效渲染(首屏呈现时间)
  • 首次可交互时间
  • 用户操作响应时间
  • 动画生成时间

收集性能数据

在 W3C 标准中定义了以下接口,可以简单的获取性能数据,兼容IE9:

// Get Navigation Timing entries
performance.getEntriesByType("navigation")

// Get Resource Timing entries
performance.getEntriesByType("resource")

timing

可得到上图数据。

加载优化

  • 减少图像的冗余度(WebP、CSS Sprite 等)
  • 使用网页字体代替部分图像
  • 使用长期缓存(可以将文件名哈希化)
  • 响应式动态图片加载
  • 使用渐进式的图片
  • 合理使用 clients hints
  • 静态资源合理内联(如内联关键渲染路径下的 Data URI,样式表内联)
  • Critical CSS
  • CSS link 标签上的「媒体类型」和「媒体查询」
  • 资源打包
  • 复杂系统组件化
  • 大组件加载异步化
  • 异步接口合并
  • 骨架屏
  • 大量节点的生成分块进行 (Web APP)

代码分割(code-splitting)

移动设备不仅网络延迟高而且解析 JS 也慢。考虑提取出 JavaScript 中关键的部分,对非关键部分懒加载。PRPL (Push, Render, Pre-cache, Lazy-load) 就是一种典型的代码分割(code-splitting)方法:

PRPL模式

当然这个模式不是一成不变的,之前流行的 Pjax 就是此模式的另一种实现。

关键渲染路径

浏览器渲染页面前需要先构建 DOM 和 CSSOM 树,合并成渲染树,然后用于计算每个可见元素的布局(Gecko 里面叫 reflow),并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。

  • 字节 → 字符 → 令牌 → 节点 → 对象模型
  • HTML 标记转换成 DOM;CSS 标记转换成 CSS 对象模型 (CSSOM)
  • DOM 树与 CSSOM 树合并后形成渲染树,渲染树只包含渲染网页所需的节点
  • 布局计算每个对象的精确位置和大小
  • 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上

render-tree-construction

优化关键渲染路径就是指最大限度缩短执行上述 5 步耗费的总时间。

在渲染树构建中,我们看到关键渲染路径要求我们同时具有 DOM 和 CSSOM 才能构建渲染树,即 HTML 和 CSS 都是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端。

为此,可将脚本标记为异步:

defer-async

浏览器还使用了「预加载器」提前发现资源,这一技术不影响以上的优化策略。

Safari 浏览器(包括苹果手机)在这方面行为特殊,可以考虑将 Script 加载完全异步化

渲染优化

完整的渲染流程有五个:JS / CSS > 样式 > 布局 > 绘制 > 合成,通过优化可以绕过某些流程。

CSS Triggers 展示了不同浏览器内核对渲染的大致处理情况,具体的情况需要自己在浏览器中调试。

high-perf-anims

  • 除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制(可用 FLIP 优化)
  • 绘制通常是像素管道中开销最大的部分,应尽可能避免绘制
  • 通过层的提升和动画的编排来减少绘制区域
  • 避免重新布局(如图片加载时撑开布局,JS 引起的重绘)
  • 用较新的布局模型

绘制并非总是绘制到内存中的单个图像,在必要时浏览器可以绘制到多个图像或合成器层。此方法的优点是,定期重绘的或通过变形在屏幕上移动的元素,可以在不影响其他元素的情况下进行处理。各个层可以在彼此的上面处理并合成,以创建最终图像,这就是所谓的硬件加速。

自动创建合成层(Composite Layer)的情况有:

  • 硬件加速的元素(video,flash,3D 等)
  • 较高 DPI 屏幕下,fixed 定位的元素会自动地被提升到合成层中。但在 DPI 较低的设备上却并非如此,因为这个渲染层的提升会使得字体渲染方式由子像素变为灰阶
  • 应用了部分 CSS3 动画
  • 有合成层后代同时本身 overflow 不为 visible 等
  • overlap 重叠等原因
    • 重叠或者部分重叠在一个合成层之上
    • 假设重叠在一个合成层之上(元素有一个 z-index 较低且包含一个合成层的兄弟元素)

手动创建新层的最佳方式是使用 will-change CSS 属性,这是一个为 web 开发者提供的一种告知浏览器该元素会有哪些变化的方法。此方法目前在 Chrome、Opera 和 Firefox 上有效,并且通过 transform 的值将创建一个新的合成器层:

有种情况容易踩坑,如果要用类似 transform: translate3d() 的方式开启 GPU 硬件加速,要注意元素层级的关系,尽量保持需要进行 CSS 动画的元素的 z-index 保持在页面最上方。

.moving-element {
  will-change: transform;
  /* 如果不支持 */
  transform: translateZ(0);
}

用好这个属性不是很容易:

  • 层不能过多或过大,这会造成 CPU 和图形处理器之间出现瓶颈,导致「层爆炸」问题,而且移动设备往往显存不太够。在浏览器开发者工具中可以看到层提升的原因。

  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。

  • 给它足够的工作时间:浏览器需要在变化发生之前去做一些优化工作(如层提升)。

  • 形成新的层叠上下文:该属性使用的任何值,都会导致元素创建一个新的层叠上下文(stacking context)。

避免强制同步布局

首先 JavaScript 运行,然后计算样式,然后布局。但是,可以使用 JavaScript 强制浏览器提前执行布局。这被称为强制同步布局

在 JavaScript 运行时,来自上一帧的所有旧布局值是已知的,并且可供查询。因此,要在帧的开头得到一个元素的高度,可能编写了如下代码:

requestAnimationFrame(() => {
  box.classList.add('super-big')
  console.log(box.offsetHeight)
})

浏览器为了回答高度问题,必须先应用样式更改,然后运行布局,这是不必要的。因此,始终应先读取样式(浏览器可以使用上一帧的布局值),然后执行写操作:

requestAnimationFrame(() => {
  console.log(box.offsetHeight)
  box.classList.add('super-big')
})

另外,在最快的情况下,当用户与页面交互时,页面的合成器线程可以获取用户的触摸输入并直接使内容移动。这不需要主线程执行任务,主线程执行的是 JavaScript、布局、样式或绘制。

但是,如果附加一个输入处理程序,例如 touchstart、touchmove 或 touchend,则合成器线程必须等待此处理程序执行完成,因为你可能选择调用 preventDefault() 并且会阻止触摸滚动发生。即使没有调用 preventDefault(),合成器也必须等待,这样用户滚动会被阻止,这就可能导致卡顿。

与滚动和触摸的处理程序相似,输入处理程序被安排在紧接任何 requestAnimationFrame 回调之前运行。即在 requestAnimationFrame 回调开始时就读取视觉属性,将触发强制同步布局!

上面两个问题的解决方法相同:始终应使下一个 requestAnimationFrame 回调的视觉更改去除抖动。

其它渲染优化:

  • Passive Event Listeners
  • 使用 WebGL 加速复杂图形
  • CSS containment(较新)

HTTP 优化

周知 HTTP 1.1 存在一些问题,比如没有字段来区分请求和响应,所以即使是持久连接也只能排队,而且管线化连接也有队头阻塞问题,更何况有些服务器没有将其实现。

HTTP 是逻辑上的短连接协议,性能的关键在于低延迟而不是高带宽。HTTP/2 通过多路复用(让所有数据流共用同一个 TCP 连接,相互不阻塞)让高带宽也能真正的服务于性能提升。

这也并不是说资源合并减少请求的优化手段对 HTTP/2 完全没用,这涉及到代码的组织维护,应具体情况具体分析。

通用的优化:

  • 合理 gzip(图片无需压缩,如果用反向代理注意上游是否已压缩过)
  • 持久链接是否正确启用(通过查看 chrome 里的 connetion id)
  • 使用 chunked 方式传输
  • 后台服务使用流的方式传输可优化 TTFB(实现起来复杂)
  • 避免重定向
  • 控制域名数量
  • 控制请求数量(最好小于 10)
  • 控制 Cookie 尺寸,使 HTTP 头不大于 TCP 传输单元

如果要用 HTTPS:

可以通过 Qualys SSL Server Test 这个工具验证是否生效。

网络优化

  • 使用较新的 TCP 协议栈(比如 Linux 2.6 之后初始 CWnd 增大为 10)
  • 合理开启 tcp_nopush
  • 使用较近的服务器,优化首次字节到达时间(TTFB)
  • 使用 BGP 等多线接入服务器
  • 不限制带宽或者静态资源走 CDN

其它前端优化

  • 根据 CSS 选择过程可知,尽量用类选择器(如 BEM 规范)
  • scope hoisting,tree-shaking 等编译优化
  • intersection observer(较新)
  • Service Workers(较新且较复杂,待观望)
  • Webassembly(较新,待观望)

参考文章