作者:rosefang,腾讯 PCG 前端开发工程师
当我们在做性能优化的时候,我们究竟在优化什么?浏览器底层是一个什么架构?浏览器渲染的本质究竟是什么?哪些方面对用户的体验影响才是最大的?有没有业内一些通用的标准或标杆参考?都 1202 年了,雅虎军规还有没有用?性能分析工具都有哪些?我们怎么进行打点分析才是合适的?
本文为你一一讲解这些。了解了这些问题,可能你在做性能优化的时候才能更加得心应手。
1. 性能优化的本质
1.1 展示更快,响应更快
性能优化的目的,就是为了提供给用户更好的体验,这些体验包含这几个方面:展示更快、交互响应快、页面无卡顿情况。
更详细的说,就是指,在用户输入 url 到站点完整把整个页面展示出来的过程中,通过各种优化策略和方法,让页面加载更快;在用户使用过程中,让用户的操作响应更及时,有更好的用户体验。
对于前端工程师来说,要做好性能优化,需要理解浏览器加载和渲染的本质。理解了本质原理,才能更好的去做优化。所以我们先来看看浏览器架构是怎样的。
1.2 理解浏览器多进程架构
从大的方面来说,浏览器是一个多进程架构。
它可以是一个进程包含多个线程,也可以是多个进程中,每个进程有多个线程,线程之间通过 IPC 通讯。每个浏览器有不同的实现细节,并没有标准规定浏览器必须如何去实现。
这里我们只谈论 chrome 架构。
下面这张图是目前 chrome 的多进程架构图。
我们来看看这些进程分别对应浏览器窗口中的哪一部分:
那么,怎么看浏览器对应启动了什么进程呢?
chrome 中,我们可以通过更多->More Tools->Task Manager 看到启动的进程。
从 chrome 官网和源码,我们也可以得知,多进程架构中包含这些进程:
Browser 进程:打开浏览器后,始终只有一个。该进程有 UI 线程、Network 线程、Storage 线程等。用户输入 url 后,首先是 Browser 进程进行响应和请求服务器获取数据。然后传递给 Renderer 进程。
Renderer 进程:每一个 tab 一个,负责 html、css、js 执行的整个过程。前端性能优化也与这个进程有关。
Plugin 进程:与浏览器插件相关,例如 flash 等。
GPU 进程:浏览器共用一个。主要负责把 Renderer 进程中绘制好的 tile 位图作为纹理上传到 GPU,并调用 GPU 相关方法把纹理 draw 到屏幕上。
这里的话只是简单介绍一下浏览器的多进程架构,让大家对浏览器整体架构有个初步认识,其实背后的细节还有很多,这里就不一一展开。有兴趣可以细看这一系列文章和chrome 官网介绍。
1.3 理解页面渲染相关进程
1.3.1 Renderer Process & GPU Process
从以上的多架构,我们了解到,与前端渲染、性能优化相关的,其实主要是 Renderer 进程和 GPU 进程。那么,它们又是什么架构呢?
来看一下这张我们再熟悉不过的图。
Renderer 进程:包括 3 个线程。合成线程(Compositor Thread)、主线程(Main Thread)、Compositor Tile Worker。
GPU 进程:只有 GPU 线程,负责接收从 Renderer 进程中的 Compositor Thread 传过来的纹理,显示到屏幕上。
1.3.2 Renderer Process 详解
Renderer 进程中 3 个线程的作用为:
Compositor Thread:首先接收 vsync 信号(vsync 信号是指操作系统指示浏览器去绘制新的帧),任何事件都会先到达 Compositor 线程。如果主线程没有绑定事件,那么 Compositor 线程将避免进入主线程,并尝试将输入转换为屏幕上的移动。它将更新的图层位置信息作为帧通过 GPU 线程传递给 GPU 进行绘制。
当用户在快速滑动过程中,如果主线程没有绑定事件,Compositor 线程是可以快速响应并绘制的,这是浏览器做的一个优化。
Main Thread:主线程就是我们前端工程师熟知的线程,这里会执行解析 Html、样式计算、布局、绘制、合成等动作。所以关于性能的问题,都发生在了这里。所以应该重点关注这里。
Compositor Tile Worker:由合成线程产生一个或多个 worker 来处理光栅化的工作。
Service Workers 和 Web Workers 可以暂时理解也在 Renderer 进程中,这里不展开讨论。
1.3.2.1 Main Thread
主线程需要重点讲下。因为这是我们的代码真实存在的环境。
从上一小节 Render 进程和 GPU 进程的图中,我们可以看到有个红色的箭头,从 Recal Styles 和 Layout 指向了 requestAnimationFrame,这意味着有 Forced Synchronous Layout (or Styles)(强制回流和重绘)发生,这一点在性能方面特别要注意。
在 Main Thread 中,有这几个需要注意一下:
requestAnimationFrame:因为布局和样式计算是在 rAF 之后,所以在 rAF 是进行元素变更的理想时机。如果在这里对一个元素变更 100 个类,不会进行 100 次计算,它们会分批以后处理。需要注意的是,不能在 rAF 中查询任何计算样式和布局的属性(例如:el.style.backgroundImage 或 el.style.offsetWidth),因为这样会导致重绘和回流。
Layout:布局的计算通常是针对整个文档的,并且与 DOM 元素的大小成正比!(这点特别要注意,如果一个页面 DOM 元素太多,也会导致性能问题)
主线程的顺序始终都是:
Input Event Handler->requestAnimationFrame->ParseHtml->ReculateStyles->Layout- >Update Layer Tree->Paint->Composite->commit->requestIdleCallback
只能从前往后,例如,必须先是 ReculateStyles,然后 Layout、然后 Paint。但是,如果它只需要做最后一步 Paint,那么这就是它全部要做的事情,不会再发生前面的 ReculateStyles 和 Layout。
这里其实给了我们一个启示:如果要让 fps 保持 60,即每帧的 js 执行时间少于 16.66ms,那么让这个主线程执行的过程尽可能地少,是我们的性能优化目标。
根据主线程的这些步骤,理想的情况下,我们只希望浏览器只发生最后一个步骤:Composite(合成)。
CSS 的属性是我们需要关注一下的模块。这里有描述了哪些CSS 属性会引起重绘、回流和合成。例如,让我们给一个元素进行移动位置时:transform
和opacity
可以直接触发合成,但是left
和top
却会触发 Layout、Paint、Composite3 个动作。所以显然用 transform 时更好的方案。
但这并不是说我们不应该用 left 和 top 这些可能引起重绘回流的属性,而是应该关注每个属性在浏览器性能中引起的效果。
2. 看看经典:雅虎军规
多年前雅虎的 Nicolas C. Zakas 提出 7 个类别 35 条军规,至今为止很多前端优化准则都是围绕着这个展开。如果严格按照这些规则去做,其实我们有很多优化工作可以做,只要认真践行,性能提升不是问题。
我们来看看它 7 个分类都是围绕哪些方面展开:
Server:与页面发起请求的相关;
Cookie:与页面发起请求相关;
Mobile:与页面请求相关;
Content:与页面渲染相关;
Image:与页面渲染相关;
CSS:与页面渲染相关;
Javascript:与页面渲染和交互相关。
从上面的描述可以看到,其实雅虎军规,是围绕页面发起请求那一刻,到页面渲染完成,页面开始交互这几个方面来展开,提出的一些原则。
很多原则大家也都耳熟能详,就不全部展开了,有兴趣的同学可以去查看原文。这里主要想提一些忽略但是又值得注意的点:
减少 DOM 节点数量
为什么要减少 DOM 节点的数量?
当遍历查询 500 和 5000 个 DOM 节点,进行事件绑定时,会有所差别。
当一个页面 DOM 节点过多,应该考虑使用无限滚动方案来使视窗节点可控。可以看看google 提的方案。
减少 cookie 大小
cookie 传输会造成带宽浪费,影响响应时间,可以这样做:
消除不必要的 cookies;
静态资源不需要 cookie,可以采用其他的域名,不会主动带上 cookie。
避免图片 src 为空
图片 src 为空时,不同浏览器会有不同的副作用,会重新发起一起请求。
3. 性能指标
3.1 什么样的性能指标才能真正代表用户体验?
要衡量性能,我们必须有一些客观的、可衡量的指标来进行监控。但是客观且定量可衡量的指标不一定能反映用户的真实体验。
以前,我们会用 load 事件的触发来衡量一个页面是否加载或显示完成。但是设想会不会有这样的情况:一个页面的 load 事件已经被触发,但是却在 load 事件之后几秒才开始加载内容和渲染页面,所以这个时候,load 事件并不能真实反映用户看到内容的时刻。
在过去几年,google 团队和W3C 性能工作组致力于提供标准的性能 API 来真正衡量用户的体验。主要是从这 4 个方面思考:
思考点 | 详细内容 |
---|---|
Is it happening? | 导航是否成功,服务器是否响应了 |
Is it useful? | 是否已经渲染了足够的内容,让用户可以开始参与其中 |
Is it usable? | 用户是否可以与页面交互,页面是否处于繁忙状态 |
Is it delightful? | 交互是否流畅、自然、没有滞后反映或卡顿 |
通常有 2 种途径来衡量性能。
本地实验衡量:本地模拟用户的网络、设备等情况进行测试。通常在开发新功能的时候,实验测量是很重要的,因为我们不知道这个功能发布到线上会有什么性能问题,所以提前进行性能测试,可以进行预防。
线上衡量:实验测量固然可以反映一些问题,但无法反映在用户那里真实的情况。同样的,在用户那里,性能问题会和用户的设备、网络情况有关,而且还跟用户如何与页面进行交互有关。
有这几个类型与用户感知性能相关。
页面加载时间:页面以多快的速度加载和渲染元素到页面上。
加载后响应时间:页面加载和执行 js 代码后多久能响应用户交互。
运行时响应:页面加载完成后,对用户的交互响应时