本期精读的文章是:Front End Performance Checklist 2017
现在随着 web 应用的复杂性日益增加,其性能优化就会显得尤为必要,同时会给性能指标分析带来新的挑战,因为性能指标之间的差异性非常大,这取决于使用的设备、浏览器、协议、网络类型以及其它能够对性能产生影响的潜在因素(如:CDN、ISP、cache、proxy、firewall、load balancer、server等)。
本文提供了解决如何让网站响应更加迅速、访问更加流畅等前端性能优化问题的方法,读者们可以提供一些在实际场景中的性能优化问题以及解决方案,可泛谈优化策略,亦可针对性深入讨论某个优化方法。
文中列举了很多不同的性能优化策略、模型或方法,如下:
根据 psychological research 指出,网站最少在速度上比别人快 20%,才能让用户感觉到比别人的更快。这个速度说的并不是整个页面的加载时间,而是启动渲染时间,首次有效渲染时间,交互时间。
RAIL performance model 提出的性能优化指标:务必在用户初始操作后的 100ms 内提供反馈。考虑到存在响应时间不足 100ms 的情况,页面最迟要在 50ms 的时候,把控制权交给主线程。
针对动画,其每一帧都需要在 16ms 内完成,这样才能保证每秒 60帧(一秒/60=16.6ms),如果可以的话最好能在 10ms 内完成。
控制启动渲染时间在 1s 以内,且速度指数在 1000 以内,对于首次有效渲染时间,最好可以优化到 1.25s 以内。
不要过度使用那些酷炫的技术栈,坚持选择适合开发环境的工具,如Grunt、Gulp、Webpack、PostCSS,或者组合起来的工具。只要这个工具运行的速度够快,而且没有给项目维护带来太大问题,就够了。
在构建前端结构的时,应始终将渐进增强作为指导原则。首先设计并且构建核心体验,再完善为高性能浏览器设计的高级特性的相关体验。
最好使用那些支持服务器端渲染的框架,如Angular,React,Ember 等。所选的框架要保证是被广泛使用并且经过考验的。不同框架对性能有着不同程度的影响,同时对应着不同的优化策略,所以要清楚的了解所选择框架的每个方面。
根据网站的动态数据量,可以将部分内容给静态网站生成工具生成一个静态版本,将其置于 CDN 上,从而避免数据库的请求,亦可选择基于 CDN 的静态主机平台,通过交互组件丰富页面。
将网站的所有文件(js,图片,字体,第三方 script 文件,多媒体内容等)进行分门别类。根据优先级区分基础核心内容,高性能浏览器设计的升级体验,附加内容等。具体细节可参考 Improving Smashing Magazine’s Performance。
使用 cutting-the-mustard 技术能够实现不同类型的浏览器载入不同类型的资源(传统浏览器载入核心型资源,现代浏览器载入增强型资源)。在载入资源时要严格遵守相应的规则:页面加载时应首先载入 Core 资源,然后在 DomContentLoaded 事件触发时载入 Enhancement 资源,最后在 Load 事件触发时载入 Extras 资源。
需要正确设置 expires、cache-control、max-age以及其它 HTTP 缓存响应头。请使用 Cache-control: immutable,可以参考 Heroku’s primer on HTTP caching headers、HTTP caching primer以及缓存之最佳实践。
想要在不等 js 执行完就开始渲染页面,可以通过在 HTML 的 script 标签上添加 defer 以及 async 属性来实现。减少第三方库和脚本的使用,尤其是社交网站的分享按键和 iframe 嵌入等。
<picture>
为了保证能够让浏览器快速渲染,会将所有用于首屏渲染的 CSS 文件整合成一个文件(即 critical CSS),以 <style>
的行内形式内嵌到 <head>
,这样可以减少 critical 渲染路径。由于 HTTP 数据包大小的限制,因此 critical CSS 文件大小不能超过14KB。
HTTP/2 协议可以让 critical CSS 用单个 CSS 文件存储,通过服务器推送 CSS 文件的传输方式来减少HTML 文件数据量,由于存在高速缓存问题,因此需要建立带有缓存的 HTTP/2 服务器传输机制。
可以通过使用 css containment 属性的方式来达到隔离性能开销大的组件,限制浏览器样式的范围,限制作用在 canvas 以外的布局和绘制工作中,限制用在第三方工具上,以确保页面滚动和出现动画效果时没有延迟。推荐使用 CSS 属性 will-change,该属性能够在元素的属性改变之前通知浏览器。
需要衡量浏览器在处于运行时渲染模式下的性能,可以参考浏览器渲染优化、如何正确的使用 GPU。
从目前来看,浏览器对 HTTP/2 支持度还不错,使用 HTTP/2 后,就可以利用 service worker 以及 HTTP/2 的服务器推送功能来获取更显著的性能提升。
在项目进行 HTTPS 改造时,需要评估 HTTP/1.1 项目的用户基数,需要针对这类用户构建并发送符合HTTP2规范的报头。
需要在载入大模块以及并行载入小模块之间找到一个平衡点。
需要检查是否正确设置 HTTP 请求头部,如 strict-transport-security,使用 Snyk 工具排除已知的漏洞以及使用 SSL Server Test 网站来检查证书是否失效。尽量保证从外部引入的插件以及 js 脚本的载入是通过 HTTPS 协议的,发起 HTTP 请求同时设置 strict-transport-security 以及 content-security-policy HTTP请求头。
通过 Is TLS Fast Yet 来查看不同服务器和 CDN 对 HTTP/2 的兼容情况。
激活服务器的 OCSP stapling,可以减少 TLS 握手所需的时间,加速 TLS 握手过程。
因为 IPv6 自带 NDP 以及路由优化,能够让网站的载入速度提升 10%-15%。
如果网站使用了 HTTP/2,需要检查服务器有没有执行 HPACK 对 HTTP 的响应头进行压缩,来减少不必要的消耗。
如果网站切换到 HTTPS,可以使用 pragmatist-service-worker 通过 service worker cache 来缓存静态资源、离线页面等,也可以从缓存中拿数据。参考当前浏览器对 service worker 的支持程度。
在 DevTool 中选一个调试工具来对每一个功能进行检查,确保知道如何分析渲染性能和控制台输出、明白如何调试 JS 以及编辑 CSS 样式。参考开发者工具的调试技巧。
完成 Chrome 和 Firefox 的测试是不够的,还需要关注部分区域占比较高的浏览器,如 UC 浏览器、Opera Min 等, 也需要了解一下受关注国家的平均网速。
在进行快速、无限制的测试时,最好使用一个个人的WebPageTest实例。建立一个能自动预警的性能预算监听。建立自己的用户时间标记从而测量并监测具体商用的数据。使用SpeedCurve对性能的变化进行监控,同时利用New Relic获取WebPageTest没法提供的数据。SpeedTracker,Lighthouse和Calibre都是不错的选择。
部署私密的 WebPageTest 测试环境,有助于快速构建测试用例。针对性能开销大的环节建立自动报警机制,可以使用 SpeedCurve 对性能的变化进行监控,利用 New Relic 获取 WebPageTest 无法提供的数据。
这一部分会介绍一些上述没有提到的方法,主要是利用 Devtools 工具对性能优化策略或方法进行深入的解读和分析。
页面代码被转换成屏幕上显示的像素,这个转换过程可以简单归纳为以下流程,包含五个关键步骤:
通过 Chrome Timeline 对页面进行 Record,其中绿色波浪线就是页面的帧率。波浪线越高表示帧率越高,反之亦然,帧率区域上边标红一行区域,表示有问题的帧,凡是标红的帧都是存在问题的,排查问题时,需要着重关注帧率低和标红的区域。
需要逐一排查带红色角标的帧,即是有问题的帧:
点击选中该帧,可以看到详细的耗时和简单的问题描述:
如果发现运行时间很长的 JavaScript 代码,则可以开启 DevTools 中 JavaScript profiler 选项,可以看到页面中的函数调用链路,就能分析出 JavaScript 代码对于页面渲染性能的影响,从而发现并修复 JavaScript 代码中性能低下的部分。那么如何修复 JavaScript 代码中性能问题呢?
假设页面上有一个动画效果,想在动画刚刚发生的那一刻运行一段 JavaScript 代码。那么唯一能保证这个运行时机的,就是 requestAnimationFrame。而大部分代码都是用 setTimeout 或 setInterval 来实现页面中的动画效果。这种实现方式的问题是,setTimeout 或 setInterval 中指定的回调函数的执行时机是无法保证的,如果是在帧结束的时候被执行,就意味着可能失去这一帧的信息,也就是发生 jank。
JavaScript 代码是运行在浏览器的主线程上的。与此同时,浏览器的主线程还负责样式计算、布局,甚至绘制等的工作。可以想象,如果 JavaScript 代码运行时间过长,就会阻塞主线程上其他的渲染工作,很可能就会导致帧丢失。
因此,需要规划 JavaScript 代码的运行时机和运行耗时,或在浏览器空闲的时候来来运行更多的 JavaScript 代码。
也可以把纯计算工作放到 Web Workers 中做,前提是这些计算工作不会涉及 DOM 元素的存取。一般来说,JavaScript 中的数据处理工作,如排序或搜索比较适合这种处理方式。
如果 JavaScript 代码需要存取 DOM 元素,即必须在主线程上运行,那么可以考虑批处理的方式,把任务细分为若干个小任务,每个小任务耗时很少,各自放在一个 requestAnimationFrame 中回调运行。
render 部分包括 Recalculate Style 和 Layout,如果发现 render 部分耗时较长,需要分别从这两部分进行分析。如果这一帧,触发了强制 layout,Timeline 会用红色角标标出,这是需要进行优化的地方。
如果需要具体分析 Recalculate Style,可以选中 Recalculate Style 部分,查看受影响的元素个数、触发 Recalculate Style 函数以及警告提示。
如果需要分析 Layout,可以选中 Layout 部分,同 Recalculate Style 一样。
那么如何提升 Render 部分的性能问题呢?
添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下),这就是所谓的样式计算。
因此需要减少执行样式计算的元素的个数,降低样式选择器的复杂度,使用基于 class 的方式,如以BEM (Block, Element, Modifier)的方式编写 CSS 代码,能达到最好的样式计算的性能,因为这种方式建议对每个DOM元素都只使用一个样式class。
布局,就是浏览器计算 DOM 元素的几何信息的过程:元素大小和在页面中的位置。
Paint(绘制)其实是生成元素呈现的像素的过程。在页面的整个被解析、执行、渲染的过程中,Paint 通常来说是代价最高的一步,因此尽量减少 Paint 时间,甚至避免 Paint 的发生,对页面性能的提升有着很重要的作用。
Timeline 中绿色部分就是 Paint 部分,Summary 会展示绘制的总体情况,包括绘制的元素、元素本身绘制耗时、元素子元素绘制耗时。如果发现绘制的区域超过了本来期望的区域,那么就是需要优化的。更加详细的信息,可以切换至 Paint Profiler,包括了每个具体 Paint 的调用和 Paint 区域截图。当页面发生 Paint 时,如果发现不期望的区域进行了 Paint,那么这里就是可以优化的。
渲染层合并,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。
提升为合成层简单说来有以下优点:
合成层的好处是不会影响到其他元素的绘制,因此,为了减少动画元素对其他元素的影响,从而减少 paint,可以把动画效果中的元素提升为合成层。提升合成层的最好方式是使用 CSS 的 will-change 属性。
创建一个新的合成层并不是无消耗的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,合成层带来的性能改善,可能远远赶不上过多合成层开销给页面性能带来的负面影响。同时,由于每个渲染层的纹理都需要上传到 GPU 处理,因此还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。
同合成层重叠也会使元素提升为合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况。因此显式声明的合成层,还可能由于重叠原因不经意间产生一些不在预期的合成层,极端一点可能会产生大量的额外合成层,出现层爆炸的现象。
现在随着 web 应用的复杂性日益增加,其性能优化的重要性越来越突出,且性能优化的方法、技巧、工具也越来越丰富和复杂,本文所展示的内容仅仅只是管中窥豹,希望读者们可以在此讨论一些在实际场景中的性能优化问题以及解决方案。
讨论地址是:精读《2017前端性能优化备忘录》 · Issue #39 · dt-fe/weekly