Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

渲染

1. 多线程模型

浏览器每创建一个 tab,就会创建一个进程,该进程间由如下的多线程模型构成:

  • 主线程

    • 可以理解为下面互斥的 GUI 渲染线程 和 JS 引擎线程,在执行谁,谁就是主线程
  • GUI 渲染线程(主线程)

  • 负责页面渲染,包括 HTML 和 CSS 解析,构建 DOM 树、布局和绘制等

    • 与 JS 引擎线程互斥,所以 js 代码如果堵塞,页面就会很卡
    • 任务队列空闲时,主线程才会执行 GUI 渲染
  • JS 引擎线程(主线程)

    • 负责处理 JS 脚本和代码,包括各种异步事件的回调
    • 与 GUI 渲染线程互斥(虽然实现上属于不同线程,但为了怕互相影响,特意做成像是一个线程一样互斥)
  • 事件触发线程(工作线程)

    • 负责将事件加入到任务队列尾部,包括用户触发点击事件,下面的 定时器到期 和 异步请求成功
  • 定时器触发线程(工作线程)

    • 负责异步定时器的计数
    • 执行代码时如果遇到定时器,就交给该线程处理,计数完毕后,就通知事件触发线程将回调加入任务队列尾部,等待 JS 引擎线程执行
  • 异步 http 请求线程(工作线程)

    • 负责异步请求的监测
    • 执行代码如果遇到异步请求,就交给该线程处理,当监测到状态码变更,就通知事件触发线程将回调加入任务队列尾部,等待 JS 引擎线程执行
  • web works 等用户执行耗时任务的线程(工作线程)

    • 用户使用 web worker 单独开启的计算任务线程,为了不让耗时任务在主线程中造成堵塞

2. 渲染机制

  • 主线程解析 HTML 生成 DOM 树

    • HTML parser 会解析 html 文档,将标签转换成 DOM node(包括 js 生成的标签),根据父子关系生成 DOM 树
  • 主线程解析 CSS 生成 CSSOM 树

    • CSS Parser 会解析 css 样式(包括 js 生成的和外部 css 引入的,也包括 html 中表示样式的如 b 标签),生成 CSSOM 树(不包含 display 为 none 的节点,也不包含 head 节点),其中每一个节点都有自己的 style
  • 主线程结合二者,生成一棵渲染树(Render Tree)

  • 计算布局

    • 主线程对渲染树从根节点开始递归调用,计算每一个元素的大小和位置,给出每个节点应该在屏幕上出现的精确坐标,生成布局树(Layout Tree)
  • 分层

    • 主线程为了处理 z 维度的元素覆盖顺序,可能需要生成多个图层树(Layer Tree)
  • 绘制

    • 合成线程得到对应的层,会按视窗大小进行分割,生成要显示的分块,交给光栅化线程
    • 光栅化线程对分块进行光栅化,真正输出为像素点,存储到 GPU 中
    • 合成线程对光栅化结果合并成合成帧,发给 GPU 完成显示
    • 以上两种线程都是在主线程之外独立完成的

3. 重绘与重排

什么叫重绘(repaint)?

  • 就是把绘制重新进行一遍
  • 触发条件:当部分元素的外观属性发生变化,但不影响布局,如颜色,可见性

什么叫重排(reflow)?

  • 就是把计算布局+分层+绘制重新进行一遍
  • 触发条件:当部分元素的几何属性发生变化,同时影响到其他元素的布局更新,如宽高,位置

触发重排的场景

  • 页面初始化渲染(不可避免)

  • 添加或删除可见的 DOM 元素

  • 元素位置改变,或者使用动画

  • 元素尺寸改变,如大小,边距

  • 浏览器窗口尺寸变化,如 resize 事件

  • 填充内容的改变,如文本内容或图片大小改变

  • 读取某些元素属性时:

    • offsetLeft/Top/Width/Height
    • clientLeft/Top/Width/Heigh
    • scrollLeft/Top/Width/Heigh
    • width/height
    • getComputedStyle
    • getBoundingClientRect

【问】 第 7 点中为何获取也会导致重排?

【答】因为浏览器通过队列来批量更新布局,修改操作会被排到队列中,至少一个刷新(16.6ms)才会清空队列。 但是当获取某些属性时,队列中可能会有影响这些属性的操作,即使没有,浏览器也会强制清空队列,触发重排

如何减少重排和重绘

四个使用,四个避免,五个尽可能

  1. 使用 transform 替代 margin-top/left
正确
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
错误
margin-top:-$(this).height()/2;
margin-left:-$(this).width()/2;
  1. 使用 visibility 替代 display
正确
visibility:hidden;
错误
display:none;
  1. 使用 class 代替多个 style

改 class 可一次性改变节点的所有属性,而不要一个个去改 style

正确
ele.className = "replaceclass";
ele.setAttribute("class","replaceclass");
ele.className += 'addclass';
ele.removeClass("oldclass");
错误
ele.style.height = '100px';
ele.style.textAlign = 'center'
ele.style['text-align'] = 'center'
ele.setAttribute('height',100) //只用于个别属性
ele.setAttribute('style', 'height: 100px !important');
ele.style.setProperty('height', '100px', 'important');//更适合强制场景
  1. 使用 documentFragment 代替多个 DOM 操作
新建对象;
var fragment = document.createDocumentFragment();

逐一填入;
for (let i = 0; i < 1000; i++) {
  var li = document.createElement("li");
  li.innerHTML = "apple" + i;
  fragment.appendChild(li);
}

一次性进行DOM操作;
document.getElementById("fruit").appendChild(fragment);
  1. 避免使用 table 布局

table 及其内部元素,可能需要多次计算才能确定节点的属性值,比同等元素多花两倍时间,而一个小改动都可能造成整个重新布局

  1. 避免设置多层样式

div > a > span{ },这样要做 3 层判断,递归过程复杂,要尽量避免过于具体的选择器

  1. 避免使用 css 表达式

表达式可能会多次计算,引发重排

  1. 避免频繁读取引发重排的属性

多次使用的话,最好用一个变量先缓存下来

  1. 尽可能把动画效果放到 absolute 或 fixed 的元素上

  2. 尽可能把频繁重排的节点设置成图层

图层可以阻止节点的渲染影响别的节点,比如 will-change,video,iframe 等标签,浏览器会自动把这些标签节点变为图层

  1. 尽可能先隐藏操作再统一显示

进行多种页面操作前,先display:none全部隐藏,操作完成后,再display:block统一显示,这样只会触发两次重排

  1. 尽可能在 DOM 树最末端改变 class

这样能限制重排范围,减小影响

  1. 尽可能开启 css3 硬件加速

开启后,对于transform,opacity,filters等动画属性不会引起重排