...

Web前端性能优化深度解读

2022-01-29

导读: 用户体验是web产品非常重要的部分,核心是让用户使用舒服,帮助用户流畅地得到所求,用户体验的优劣甚至会影响到用户的留存。体验差的网站各有各的不同,但是体验好的网站往往都有一些共性,这些优秀的特征凝结了设计师、研发工程师和产品经理的大量智慧。

  • 访问交互速度迅速

  • 动画效果顺滑流畅

  • 有用户操作的反馈

  • 简单的操作步骤

  • 整站体验一致性

  • 主体内容在最显眼的位置

  • 无障碍访问,不同的人群均可使用

在这些优秀体验的特性中,最容易让人产生共鸣的往往是网站的性能问题,比如网站的访问交互速度。如何发现性能问题?性能如何优化(性能优化的常规方法和框架方法)?如何衡量收益?本文根据多年在性能优化方面的实践,着重分享一下首屏性能优化的一些经验。

01 性能采集

工欲利器事,必先利其器。我们所说的性能采集并不是性能分析Devtools,而是指在产品真实用户访问的大数据中进行抽样,对于抽样用户进行性能数据采集,得到真实用户环境下产品性能数据。各浏览器厂商都已认识到性能对于web开发的重要性,为了解决当前性能测试的困难,W3C推出了一套性能API标准,目的是简化开发者对网站性能进行精确分析与控制的过程,方便开发者采取手段提高web性能。整套标准包含了10余种API,在下图中可以看到它们当前在规范流程中的进展。

图:性能API标准(摘录51CTO图片)

这套标准中提供了导航定时(Navigation Timing)、资源定时(Resource Timing)、用户定时User Timing和性能时间线(Performance Timeline)规范可以帮助开发人员精确地测量文档的导航时间,在页面上获取资源的情况,以及开发人员脚本执行情况。

在这套API中,页面加载Navigation Timing和页面资源加载Resource Timing这两个API可以帮助我们获取页面的Domready时间、onload时间、白屏时间以及单个页面资源在从发送请求到获取到response各阶段例如带宽、延迟或主页的整体页面加载时间的性能参数,这些都是基于真实用户数据(RUM)。

图:Navigation Timing关系图(摘录W3C)

在获取用户访问Timing数据的前提下,我们可以结合具体业务场景定义访问性能的核心指标,例如白屏时间、首屏时间FSP、用户可交互时间TTI、页面onload时间等作为核心优化指标,其中首屏时间和用户可交互时间需要单独埋点自定义。

还可以通过获取DNS查询耗时、TCP链接耗时、request请求耗时、解析dom树耗时、白屏时间、domready时间、onload时间等做性能分析,后续根据症状对这些细致阶段做性能优化,这些参数是通过上面的performance.timing各个属性的差值组成的。

通过使用API对各个阶段性能指标进行采集,等待到所有数据都获取完成之后,通过网络请求将数据发送到服务器用作后续数据分析使用。

02 性能优化

快速加载、及时响应用户反馈、提供流畅的动画、以及拥有类似原生APP一般沉浸的用户体验是web应用在性能优化上的目标,这主要关系到加载性能和渲染性能两个方面,本章节介绍一些常规优化方法和框架级优化方案。

2.1 加载性能优化

Web 页面通常由 HTML、CSS、JavaScript 和其他多媒体资源组成,充斥着各种同步资源和异步资源。页面加载时,必须从服务器获取这些资源。

2.1.1 减小资源体积

  • 压缩文本内容

  • 优化JavaScript第三方库引入

压缩虽然简单,但十分有效,这也是最广泛的优化资源体积的操作。许多工具可以帮助我们完成HTML、CSS、JavaScript、图片等压缩。例如,TerserPlugin可以用于压缩 JavaScript,PostCSS可以对 CSS 进行压缩,以及完成前缀自动补全工作。除了压缩单个文件外,在服务器上配置 Gzip 也十分重要。Gzip 对文本资源的压缩效果非常明显,通常可以将体积再压缩至原本的 30% 左右,但 Gzip 对已经单独压缩的图像等非文本资源来说,效果并不好。

如果我们只需要使用工具库中少数几个简单函数,可以考虑使用原生 JavaScript 代替。不计后果地引入第三方库,会迅速增大 JavaScript 资源的体积。

2.1.2 对资源进行缓存

缓存在优化页面加载性能的工作中有举足轻重的作用,缓存无处不在,包括浏览器端、网络代理、服务端缓存,往往能大幅加快响应速度。

图:web全链路缓存

  • HTTP 缓存

  • Local Storage

  • Cache Storage

  • IndexedDB

  • CDN

现代浏览器都实现了 HTTP 缓存机制。浏览器在初次获取资源后,会根据 HTTP 响应头部的Cache-Control和ETag字段,来决定该资源的强缓存策略或者协商缓存策略。

Local Storage主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小。

Cache Storage它用来存储 Response 对象的,也就是说用来对 HTTP响应做缓存的,通常在PWA技术中使用。

IndexedDB是一种在浏览器中持久存储数据的方法,允许我们不考虑网络可用性,创建具有丰富查询能力的可离线web应用程序。

内容缓存在CDN网络节点,位于用户接入点,是面向最终用户的内容提供设备,可缓存静态Web内容和流媒体内容,实现内容的边缘传播和存储,以便用户的就近访问。

2.1.3 调整资源优先级

通过调整资源加载优先级,保证主体内容能够较快的被加载完成,通过预加载、懒加载等多种方式,调整资源加载的行为,优化网页加载性能。

  • 预加载

  • 预连接与 DNS 预解析

  • 预取

  • 懒加载

  • Service Worker

通过来提前声明当前页面所需的资源,以便浏览器能预加载这些资源。通过media属性进行媒体查询,根据响应式的情况选择性地预加载资源。

预连接会提前完成 DNS 解析、TCP 握手和 TLS 协商的工作,但并不会提前加载资源。也可以考虑使用,提前与资源建立 socket 连接。

浏览器会在空闲时,使用最低优先级下载预取的资源。预取通过声明,通常用于点击“下一页”的页面动作之前提前加载用户接下来可能需要的html资源。

按需加载和延时加载都属于懒加载的范畴,例如对图像资源采用“懒加载”策略,即仅加载当前在视口内的图像,对于视口外未加载的图像,在其即将滚动进入视口时才开始加载。

利用Service Worker 线程脱离在主线程之外来进行 Web 资源和请求的持久离线缓存。

2.1.4 合理拆分代码

浏览器支持并行加载资源,合理拆分资源也是一种有效的优化方法。为了更好的效果,我们往往不需要在首屏一次性加载所有 JavaScript 代码,合理的拆分代码、区分开发和生产环境使用少量主要代码,将当前暂时不需要的代码拆分出去可以有效加快首屏展现的速度。通过webpack区分开发环境和生产环境差异化配置打包资源可以有效优化代码,Tree shaking使得模块间依赖可以通过静态分析来更好地优化剪枝(仅ES modules支持)。webpack-bundle-analyzer 是一个关于 webpack 构建产物的可视化插件,可以清晰地看到构建产物的体积,帮助分析后续的优化方向。

2.1.5 HTTP/2

HTTP/2带给WEB带来了很大的性能提升,同时多路复用、头部压缩、Server Push等特点,使得可以在一个连接上同时打开多个流双向传输数据,服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。

图:http1 vs http2

2.2 渲染性能优化

浏览器在渲染页面前,首先会将 HTML 文本内容解析为 DOM,将 CSS 解析为 CSSOM。DOM 和 CSSOM 都是树状数据结构,两者相互独立,但又有相似之处。接着,浏览器会将 DOM 和 CSSOM 树合并成渲染树。从 DOM 树的根节点开始遍历,并在 CSSOM 树中查找节点对应的样式规则,合并成渲染树中的节点。在遍历的过程中,不可见的节点将会被忽略。渲染树随后会被用于布局,就是计算渲染树节点在浏览器视口中确切的位置和大小。浏览器进行一次布局的性能开销较大,我们需要小心地避免频繁触发页面重新布局。得到渲染树节点的几何布局信息后,浏览器就可以将节点绘制到屏幕上了,包括绘制文本、颜色、边框和阴影等。

绘制的过程,首先会根据布局和视觉相关的样式信息生成一系列绘制操作,随后执行栅格化(栅格化是将向量图形格式表示的图像转换成位图以用于显示器或者打印机输出的过程),将待绘制项转换为位图存储在 GPU 中,最终通过图形库将像素绘制在屏幕上。

图:浏览器渲染过程

页面不是一次性被绘制出来的。实际上,页面被分成了多个图层进行绘制,这些图层会在另一个单独的线程里绘制到屏幕上,这个过程被称作合成。合成线程可以对图层进行剪切、变换等处理,因此可以用于响应用户基本的滚动、缩放等操作,又不会受到主线程阻塞的影响。

2.2.1 关键渲染路径

由于渲染都是在主进程中执行的,所以合理的利用主进程渲染非常重要。首屏渲染所必须的关键资源,共同组成了关键渲染路径,减少非关键渲染路径的资源消耗可以有效提升渲染速度。

  • 延迟非关键 CSS 加载

  • async 和 defer

Web 应用中往往会有一些首屏渲染时用不到的 CSS,如弹框的样式等。通过引用的 CSS 都会在加载时阻塞页面渲染。为了使这些非关键 CSS 不阻塞页面渲染,可以通过拆分资源的方式并延迟非关键资源加载。

由于渲染都是在主进程中执行的,所以合理的利用主进程渲染非常重要。首屏渲染所必须的关键资源,共同组成了关键渲染路径,减少非关键渲染路径的资源消耗可以有效提升渲染速度。

2.2.2 非阻塞 JavaScript

用户对于不流畅的滚动或动画十分敏感,一般要求页面帧率应达到每秒 60 帧。由于 JavaScript 一般是单线程执行的,长时间执行的任务会阻塞浏览器的主线程,使页面失去响应,出现卡顿和假死的现象。

  • 页面滚动

  • requestAnimationFrame 任务在浏览器渲染下一帧之前执行

  • requestIdleCallback 将任务安排在浏览器空闲时执行

  • Web Workers

当我们监听 touchstart、touchmove 等事件时,由于合成线程并不知道我们是否会通过 event.preventDefault() 来阻止默认的滚动行为,从而在每次事件触发时,都会等待事件处理函数执行完毕后再进行页面滚动。这通常会导致较明显的延迟,影响页面滚动的流畅性。通过在addEventListener()时声明{passive: true},来表明事件处理函数不会阻止页面滚动,使得用户的操作更快得到响应。

我们可以将一些耗性能的逻辑放在 worker 线程中进行处理,这样主线程就能继续响应用户操作和渲染页面了。

2.2.3 降低渲染树计算复杂性

结构越复杂的页面往往性能越差,动画多的页面出现卡顿的几率也越大。

  • 减少查找与元素匹配成本

  • 减少布局次数

  • 优化绘制与合成

渲染树由 DOM 和 CSSOM 树合并而成,对于每个 DOM 元素,需要查找与元素匹配的样式规则。CSS Modules 是一种较为主流的 CSS-in-JS 解决方案,利用 webpack 等构建工具,可以对类选择器生成自定义格式的唯一类名,同样能减少浏览器匹配 CSS 选择器的开销。

浏览器进行一次布局的开销很大,所以我们需要尽可能避免直接修改这些属性,尤其是不应将布局属性用于动画效果,否则会出现明显的掉帧现象。

修改绝大多数样式属性都会导致页面重绘,这很难避免。仅有的例外是transform和opacity,这是由于它们可以仅由合成器操作图层来实现。transform和opacity非常适合用于实现动画效果,但我们仍需要通过will-change为它们创建独立的图层,避免影响其他图层的绘制。

2.3 框架优化方法

CSR、SSR、NSR、ESR、hybrid离线包、Big pipe、app cache等,都是不错的方法。

2.3.1 CSR(Client Side Render)

浏览器渲染顾名思义就是所有的页面渲染、逻辑处理、页面路由、接口请求均是在浏览器中发生,也就是从服务端请求一个简单HTML文件然后通过执行JavaScript在HTML上进行内容的添加。其实,现代主流的前端框架均是这种渲染方式,这种渲染方式的好处在于实现了前后端架构分离,利于前后端职责分离,并且能够首次渲染迅速有效减少白屏时间。同时,CSR可以通过在打包编译阶段进行预渲染或者骨架屏生成,可以进一步提升首次渲染的用户体验。

图:CSR

2.3.2 SSR(Server Side Render)

服务端渲染则是在服务端完成页面的渲染,在服务端完成页面模板、数据填充、页面渲染,然后将完整的HTML内容返回给到浏览器。由于所有的渲染工作都在服务端完成,因此网站的首屏时间和TTI都会表现比较好。

图:SSR

但是,渲染需要在服务端完成,并不能很好进行前后端职责分离,而且白屏时间也会比较长,同时,对于服务端的负载要求也会比较高。

2.3.3 NSR(Native Side Render)

GMTC2019 全球大前端技术上 UC 团队提到了 0.3 秒的 “闪开” 方案。这种方案适用于混合开发,NSR本质是分布式SSR,通过加载离线页面模板,Ajax预加载页面数据,Native渲染生成Html数据并且缓存在客户端,将服务器的渲染工作放在了一个个独立的移动设备中,实现了页面的预加载,同时又不会增加额外的服务器压力。核心思路是借助浏览器启用一个 JS-Runtime,提前将下载好的 html 模板及预取的 feed 流数据进行渲染,然后将 html 设置到内存级别的 MemoryCache 中,从而达到点开即看的效果。

图:NSR

2.3.4 ESR(Edge Side Render)

边缘渲染的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,先后返回给用户。CDN 节点相比于Server距离用户更近,有着更短的网络延时。在 CDN 节点上将可缓存的页面静态部分先快速返回给用户,同时在 CDN 节点上发起动态部分内容请求,并将动态内容在静态部分的响应流后继续返回给用户。

图:ESR

03、收益衡量

速度是应用性能最直接体现。做性能收益衡量也需要多维度全方位的进行分析与对比。通过等量实验组和对照组在核心指标方面大量真实数据的分位值对比,可以得到性能方面的收益,也可以关联到用户PV、UV以及收入等方面是数据收益。

监控网站真实用户可感知的白屏、首屏、可交互等用户体验指标,从服务器端响应时间、网络延时、DOM解析等细致指标的变化也可以做日常性能优化。

  • 统计核心指标不同分位数的占比数据。

  • 统计不同版本浏览器和设备类型的核心指标数据,基于多平台浏览器性能分析。

  • 统计不同区域(包括国家、省份、城市)、不同运营商以及接入方式(包括2G/3G/4G/WiFi)下的各关键网络性能指标。

图:性能平台

业内不错的性能监控平台包括ONEAPM、听云、性能魔方等,各个大公司和云平台也都提供不错的相关监控服务。

04 总结

你做事的时候不只是靠经验教训的历史积累,还有一套系统的流程或者模板。做性能优化是一件需要具有闭环思维的事情,特别是这种端到端的优化要注意事前规划、事中执行和事后总结三个阶段,而且还要结合不同的业务场景进行优化,有时候还要与客户端相协同,并不是生拉硬套就可以完成的事情。

甚至很多大厂的业务前端还要一边解决历史包袱,一边进行优化,小心前行!随着优化后业务仍然在不断的迭代和发展,如何巩固性能优化结果也是一件任重道远持续投入的事情,掌握性能优化基本原理结合具有优秀性能结构设计或许是一种智慧的方法。


来源:51CTO