从零开始的 Web 前端性能优化 – 规则

build delightful, modern, responsive web experiences. — Google Web Fundamentals

引言

前端的性能指标在通常情况下是一个很难量化的指标,面向的用户群体和环境复杂,在现有条件下,网页的绝对性能仍然取决于用户的网络连接。

因此本文基本参考 Google 在 PageSpeed Insights 中的标准,考量点都是与网络无关的因素:服务器配置、网页的 HTML 结构及其所用的外部资源。

实际上前端的性能优化,非常依赖于开发者的经验,技术和环境的快速发展,大多数开发者还没能建立起完善、可靠的性能优化体系。因此,我更喜欢参考大型企业的标准,来制定优化规则。

Performance

首先要提到这个单词,一般来说,它可以被翻译成高性能,不过,在前端领域,其实 Performance 还有另外一层含义,『高表现力』。

高表现力就意味着高可用性、高易用性和高度的用户友善设计。不过这些内容层次太高,本文只讨论高性能这一个环节。

大型企业的标准

从 Google 建立以来,它一直是这个世界上最顶尖的 Web 技术开发和使用者之一,我认为它所施行和推广的 Web 技术体系,是可以作为我们开发过程中的有效参考的。

首先要介绍的是两个 Google 下属的直接面向前端性能优化的项目,PageSpeed 和 Web Fundamentals。

PageSpeed

PageSpeed 是个历史悠久的项目,从建立以来就是针对高性能的网站开发。它的工作流程就是,抓取网页,按照一定的规则去分析网页,给出参考的性能评分和优化建议。

所以首先要看的就是它的规则。在性能方面,PageSpeed 的规则概括为以下几条:

在这些规则之中,有一些是已经成为成熟的方案,而被广泛采用,成为整个技术实现的基本环节,这些规则不再需要阐述,因为大家都在这么做了,比如 Enable compression (gzip 压缩)。所以下面只介绍一些比较容易被忽略的问题。

1. Avoid landing page redirects

先介绍一下 Landing Page,根据 Wiki 的定义

a single web page that appears in response to clicking on a search engine optimized search result or an online advertisement.

简单来说就是用户从另一个页面(站点)跳转到目标站点的入口页面,通常情况下就是网站的首页。所以这条规则就是避免首页的重定向

理由也很简单,最好的情况下,每一次重定向就增加一次往返(HTTP 请求-响应),而糟糕的情况下就会增加很多次。每一次的往返中包含了 DNS 查询,TCP 握手,TLS 认证环节。

以 Snow/Beef (主站退单/手淘退单)为例。为了区分不同的订单退单情况,这两个项目都使用服务器端跳转来引导客户端页面转向到对应的路由(业务逻辑)。

那么从用户的角度来说,这里就多了一次往返逻辑,用户需要多等待一次请求/响应,然后再进入对应的页面。当然,这也是因为受限于客户端(APP)的 WebView 环境而选择一种解决方案。

2. Optimize images

这个问题主要发生在一些设计素材上,为了保证高度还原而选择高质量的图片,就忽略了图片的大小。

有时候也可以适当考虑,使用 CSS 来实现,以减小图片的使用率。

讨论

如同上文所说,这些规则基本上已经成为了业界广泛采用的标准,以现在的环境来看,这些大方向上的实践已经没有太多问题。因此,如果想要更进一步的提升页面性能,那就需要更加细致的挖掘。

Web Fundamentals

Web Fundamentals 是 Google 2014 年发起的项目。

The fundamental knowledge you need to build delightful, modern, responsive web experiences.

项目总结也很简单:提供令人赏心悦目的现代化响应式 Web 体验所必须的基础知识。其中有一个章节『Performance』着重介绍了网页性能这一块的知识,并且,由于项目较新,所以讨论到的内容以及技术也适应了新的环境,比较有参考价值。

概述

这个章节这样总结提高性能的技巧:提高性能表现的过程要从最小化,或者至少从优化用户下载数据开始。提高代码效率的前提是要理解浏览器是如何渲染这些文件的。最后,你需要一些方法来测试你的优化的效果。

最小化(优化)数据内容大小,提高代码效率,测试优化效果。这些就是一个完整的优化方法。

1. Optimizing Content Efficiency

这一节都是关于如何优化『delivery』的效率,作者总结了几个要点:

对比一下之前在 PageSpeed 一节提到的优化点,这里新增了『限制不必要的下载』、『优化 Web Font』。

  1. 限制不必要的下载现在在我们的项目体系中,开源项目占了越来越大的比重,模块化的概念越来越深入,在开发过程中,我们会引入相当多的第三方资源,所以仔细检查这些资源,并认真考量它们是否有必要被引入。同时,对于多页面应用,应该检查每个页面是否只下载了它所需要的资源。 当然,还需要优化资源的大小,以及优化它的技术实现。

  2. 优化 Web Font 这种技术其实受硬件发展的影响更多一点,网络的发展,使得页面可以更快地加载越来越多、越来越大的资源,所以更加丰富的 Web Font 也流行起来。带来的问题是,资源大小的增加和页面文字重新渲染的问题。

    以金融运营平台为例,引入了 Font-awesome、Glyphicon 两种 Web Font(icon font),Font-awesome 用来处理各种图标,而 Glyphicon 是因为 ui-grid 这个库依赖于它。两个 Web Font 其实非常大,并且 Glyphicon 只有 ui-grid 使用了,并且用到的字体非常少。在后续优化中,针对 Glyphicon 的 CSS 设计,使用 Font-awesome 写了替代规则,从而移除了 Glyphicon。

    文章中其实提供了相当多的 Web Font 优化技巧,也可以作为很好的参考。

2. Critical Rendering Path

浏览器在获取 HTML,CSS 和 JavaScript 文件之后,将他们渲染成页面上每一个对应的像素,所经过的步骤就是 critical rendering path,就是也就是概述中提到的理解浏览器如何渲染页面。当然,浏览器引擎对于大多数人来说还是太复杂、太深奥了一点,我们可以了解一下简略的内容。

  1. Constructing the Object Model第一步简单来说就是构建 DOM 和 CSSOM 树。所以很显然,为了浏览器可以尽快的渲染页面,我们必须尽快地把 HTML 和 CSS 交给浏览器。这就是为什么我们说要把 CSS 资源写在 head 标签中,而把 JavaScript 资源写在 body 标签的结尾处,为了让浏览器优先获取 HTML 和 CSS,不让 JavaScript 阻塞页面渲染。

  2. Render-tree Construction, Layout, and Paint 上一部已经生成了 DOM 和 CSSOM 树,这一个步骤首先就是要把它们合并成 Render 树。要注意的是,Render 树只会包含渲染页面所需要的节点。接着 Layout 引擎会计算每一个对象的位置和大小。最后把 Render 树渲染到屏幕上。

  3. Render Blocking CSS 首先要了解,HTML 和 CSS 都是阻塞性的资源,浏览器在处理完 CSSOM 之前,不会进行任何渲染动作,所以我们保证 CSS 资源的加载速度。

    另外,Media type 和 Media query 是可以把 CSS 标记为非阻塞形式,这样就可以保证浏览器只处理需要的资源。不过即使是这样,浏览器还是需要先把所有 CSS 资源下载完成,所以,依然需要保证 CSS 资源的优化。

  4. Adding Interactivity with JavaScript 同样,JavaScript 也会阻塞 DOM 的构建,延长页面的渲染时间,所以确保只加载必须的 JavaScript 的资源,同时,不会影响到页面功能的 JavaScript 资源使用异步加载(或 async 特性),来提高页面渲染速度。

    async 这种特性,使用的比较多的,就是各类页面分析、页面监控服务的 JavaScript 库,因为它们的逻辑和页面主线功能互不影响。

到这里为止,页面的渲染就完成了,在这之后,作者还分享了针对 Critical Rendering Path 的各种优化方法,可以进一步了解学习(都包括在文末参考文献中)。

3. Rendering Performance

这一节的主要内容就是如何提升你的代码效率。毫无疑问,你需要了解浏览器处理 HTML,CSS 和 JavaScript 的方式。作者提到:高性能的页面,不仅仅是加载迅速,同时还要能正确地运行;页面滚动必须无延迟跟随手指,动画和交互必须如丝般顺滑,这就是所说的『令人赏心悦目的 Web 体验』。

绝大部分设备 1 秒钟刷新屏幕 60 次,这就是我们说的页面动画必须达到 60 fps 的原因,然后简单计算一下的话,每一帧有 16 毫秒时间,除去浏览器工作的开销,基本上我们在一帧之内有 10 毫秒的时间来完成我们的各种工作,那么,如果说某一个任务超过了 10 毫秒,就会带来一个直观的感受,『页面卡了』。

浏览器的一次工作流程就是:JavaScript > Style > Layout > Paint > Composite 所以也很容易理解,为了提升页面效率,就需要尽可能地减少浏览器的工作步骤,而在上面的五个步骤中,Layout 和 Paint 是可以被避免的,这就是我们优化的关键

  1. Optimize JavaScript Execution简单来说,在不恰当的时间点运行的 JavaScript 或者运行周期比较长的 JavaScript 代码,就会导致性能问题,这些就是优化的关键点。

    当然,首先了解到,实际上在浏览器中执行的代码和你写的 JavaScript 代码并不一样,现代浏览器都采用了 JIT 编译器来优化你写的代码,以提升运行效率,不过这是比较复杂的问题了,我们还是从简单的地方入手。

    先了解一个概念 Visual Changes,就是那些不会直接影响页面样式的代码逻辑,比如查找数据和数据排序。

    • 避免使用 setTimeout 和 setInterval 来进行 Visual Change 操作,使用 requestAnimationFrame 来进行替代。
    • 把耗时较长的 JavaScript 移动到 Web Worker 中执行,和主线程分离。
    • 细化 JavaScript 任务,使得 DOM 的变化分布到不同的帧中。
  2. Reduce the Scope and Complexity of Style Calculations这一条很好理解,降低样式的复杂度。由于每一次 DOM 的变更,浏览器都需要重新计算元素样式,进行 Layout(或者 reflow),所以降低样式复杂度,是提升页面渲染效率的很重要的一环。

    • 降低选择器的复杂度;使用以 class 为中心的 CSS 设计模式,比如 BEM。
    • 减少样式计算会涉及到的元素数量。
  3. Avoid Large, Complex Layouts and Layout ThrashingLayout 就是浏览器计算元素在页面内的位置和大小的过程。和样式计算类似,Layout 的开销收到需要进行 Layout 计算的元素的数量,以及一次 Layout 的复杂程度的影响。

    • Layout 通常情况下会包括整个 document。

    • DOM 元素的数量会影响性能;应该尽可能避免触发 Layout。

    • 评估 Layout 模型的性能;新的 Flexbox 模型通常要比旧的 Flexbox 模型以及基于 Float 的模型要更快。

    • 避免强制同步 Layout(在 JavaScript 执行之前强制浏览器执行 Layout 动作,请参考上文浏览器 render 的过程),以及 Layout 抖动;先读取样式值,然后再进行写入操作。当任何几何属性发生变更的时候(比如 width, height, left, top),都会触发 Layout。

      Layout 是开销非常大的操作,应该在可能的情况下避免它。对于 Flexbox,目前主流浏览器都已经支持了 Flex 布局,在环境允许的情况下,应当优先选择 Flexbox,并且使用新的规范。

  4. Paint Complexity and Reduce Paint Areas Paint 通常是整个渲染过程中运行时间最长的任务。

    • 除了 transform 和 opacity 之外的任何属性变更都会触发 paint。
    • paint 一般是渲染流程中开销最大的部分。
    • 通过图层提升和动画编排来减少需要 paint 的区域。
    • Chrome DevTools 可以获取 paint 的分析数据。

    这里提到了图层提升这个概念,就类似于 Sketch、GIMP、Photoshop 中的图层,不同的独立图层可以单独计算并最终合并。

    在 CSS 中,will-change 属性会创建一个新的图层,然后把值设为 transform 就会创建一个合成器层。这个方法支持 Chrome、Opera、Firefox。

    那么对于那些不支持的浏览器,就只能使用 3D 变形来创建一个新的图层。利用transform: translateZ(0) 强制进行 3D 变形。需要注意,创建新的图层会增加内存和管理开销,所以需要合理安排图层。

  5. Stick to Compositor-Only Properties and Manage Layer Count 这一节深入探讨了合成器的概念,如果对于动画性能优化有兴趣,可以深入了解一下。

  6. Debounce Your Input Handlers 对各种输入操作的处理进行防抖。输出操作的处理是一个潜在的性能问题,他们可能会阻塞帧的计算,并且增加额外的、不必要的 Layout 任务。

    • 避免长时间的输入操作处理任务;它们会阻止页面滚动。
    • 不要在输出操作处理任务中进行样式变更。
    • 对处理任务进行防抖;存储事件参数,并且在下一个 requestAnimationFrame 的回调中进行样式变更。

    这里举出实际的例子就是,如果监听了 touchstarttouchmovetouchend 之类的事件,并且调用了 preventDefault() 那么合成器线程就必须要等待你的事件回调结束,在这段时间内,滚动操作就会被阻止。

    事实上,即使你没有调用 preventDefault() 合成器也还是要等待,就会导致用户的滚动操作被锁住,出现丢帧之类的问题。

从规则到实践

以『前端』、『性能』、『优化』之类的关键词 Google 一下,可以得到数不清的结果,关于各类优化的奇巧淫技,不需要累述,我们更需要关注的是,如何有效、高效、科学地进行性能优化,就如同我们优化页面性能一样,去优化我们的开发过程。

参考文献

  1. PageSpeed Insights Rules
  2. Web Fundamentals – Performance