前端性能优化-从浏览器的渲染机制看性能优化

313 阅读8分钟

在了解浏览器的渲染机制前,我们来看看一个非常重要的浏览器内核知识。

浏览器内核

浏览器内核也可以称为渲染进程,核心的部分是“渲染引擎”,主要包括以下线程:

kernel

1. GUI渲染线程

  • 解析 HTML, CSS 为 DOM Tree和Style Tree, 进行布局和绘制为网页

  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

  • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行

2. JS 引擎线程

  • 如V8 引擎就是 JS 引擎线程,负责 JS 的解析和编译

  • GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,就会造成页面的渲染造成阻塞,导致页面渲染卡或时间过长

3. 事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理

4.定时触发器线程

浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时

5.异步http请求线程

http请求线程

从上面的浏览器内核我们知道了GUI 渲染线程与 JS 引擎线程是互斥的,所以我们应该尽量减少js对dom的直接或频繁操作。

浏览器渲染过程

接着看看浏览器的渲染过程图解1:

brower1

GUI渲染线程工作图解2:

GUI

从图解中可以看出:

浏览器会解析三个东西:

  • 一是HTMLL,浏览器会把HTML结构解析转换DOM树形结构

DOM Tree

  • 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像

Style Rules

  • 三是Javascript脚本,等到Javascript 脚本文件加载后, 解析js,通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。

Javascirpt

阻塞渲染:

阻塞渲染,仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪

CSS的阻塞渲染

从GUI渲染线程工作图解2中可以看到,Render Tree 需要依赖 DOM Tree和 Style Rules,所以CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕

在外网样式资源下载完成前,页面将会处于白屏现象

如阻塞渲染的样式资源下载超时报错,则会跳过,会使用已经下载完成的CSS资源做解析构建CSSOM

所以在等待一段时间后(资源下载超时后)页面才会显示出来

JS的阻塞渲染

JavaScript 可以查询和修改 DOM 与 CSSOM

所以当 HTML 解析器遇到一个 script 标记时,它会暂停GUI渲染线程构建 DOM,将控制权移交给 JS 选择引擎,等 JS 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建

解除阻塞

将 JavaScript 脚本显式声明为异步,即可防止其阻塞DOM构建与渲染

向 script 标记添加异步关键字可以指示浏览器在等待脚本可用期间不阻止 DOM 构建

  • defer:异步进行下载,然后等待 HTML 解析完成后(DOM完成构建)按照下载顺序进行执行

  • async:异步进行下载,下载完成后会立即执行,执行时仍然会阻塞

使用preload来声明提前加载css和js资源:
  • 因为css和js资源的下载都会阻塞渲染,使用preload来声明提前加载css和js资源

  • preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),需要执行时再执行

  • css引入标签放在head里面,js引入标签放到最后面;使用preload已经加载好了资源,遇到引入标签了才执行

@vue/cli里面打包就是这样使用的:

<head>
  <meta charset="utf-8">
  <link href="/vuejs-loadmore/css/app.018dd75a.css" rel="preload" as="style">
  <link href="/vuejs-loadmore/js/app.3384a5cf.js" rel="preload" as="script">
  <link href="/vuejs-loadmore/js/chunk-vendors.69ad0f2e.js" rel="preload" as="script">
  <link href="/vuejs-loadmore/css/app.018dd75a.css" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
  <script src="/vuejs-loadmore/js/chunk-vendors.69ad0f2e.js"></script>
  <script src="/vuejs-loadmore/js/app.3384a5cf.js"></script>
</body>

从浏览器的渲染过程看优化

  • html结构不宜嵌套太深
  • css尽可能快的加载,首屏可以只加载首屏用到的css,其他css不要一次性加载进来,必要时使用内联引入;css会影响js的解析,css必须放在js前面引入
  • js尽可能最小化,不要以为使用了Gzip后js体积得到了极大压缩,但是js解析编译耗时很长,不涉及到当前页面的js被加载进来编译会增加额外耗时
  • 使用preload来声明提前加载css和js资源
  • 尽量减少js对dom的直接或频繁操作;vue或者react都是操作虚拟dom后再放入页面生成真实dom

浏览器的关键渲染路径

由上面浏览器的渲染过程我们可以总结一下浏览器的关键渲染路径:

main

  • 通过JavaScript操作dom/css,或者用css动画来触发视觉变化
  • 有了视觉变化后,浏览器要重新对样式进行计算,计算哪些元素的css收到影响
  • 布局Layout/回流Reflow:布局将元素变化的样式绘制到页面上,变化的样式包含width等影响布局的样式,background等不影响页面布局的样式变化不会引起Layout的触发,只会产生Paint
  • 绘制Paint:将内容画到页面上,也就是绘制
  • 复合图层Composite: 浏览器将不同的内容绘制到不同的层上,最后合成在一起呈现在页面上

布局Layout和绘制Paint

布局和绘制是关键渲染路径中最重要的两步,也是浏览器开销最高的。所以减小布局和绘制的发生,甚至可以避免布局和绘制,就可以很大程度优化性能。

布局Layout也可以称为回流Reflow;布局根据渲染树计算每个节点精确的位置和大小,绘制将内容画到页面上,来看看引起回流重绘的操作分别有哪些:

影响回流重绘的操作

  • 添加或删除可见的DOM元素
  • display: none
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 修改浏览器大小,字体大小 ...

注意:回流一定会触发重绘,而重绘不一定会回流

浏览器的优化机制

由于每次重排都会造成额外的开销,大多数现代浏览器都会使用队列来批量执行优化重排过程;浏览器会将修改操作放到队列里,直到一段时间或者到了一个阈值,才执行并清空队列。但是,当有获取布局信息的操作的时候,会强制队列刷新,比如访问一下属性或方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • getBoundingClientRect
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • 具体可以访问这里 gist.github.com/paulirish/5…

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值

所以我们在修改样式的时候,如果要使用它们,最好将值缓存起来

减少回流和重绘

下面看看如何减少回流和重绘。

避免触发强制同步布局事件

上面说过,当获取元素的一些布局属性,会导致浏览器强制清空队列,进行强制同步布局。

例如:我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。

css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦。

css3硬件加速不会触发Layout和Paint,只会触发合成图层Composite Layers.

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

参考链接(Tanks):

你不知道的浏览器页面渲染机制

从 8 道面试题看浏览器渲染过程与性能优化

浏览器专题系列 - 浏览器内核

浏览器专题系列 - 阻塞渲染

你真的了解回流和重绘吗

preload与prefetch的使用和区别