性能优化之渲染优化

本文最后更新于:2021/07/05 , 星期一 , 21:32

浏览器渲染过程(对应面试题:从用户输入url到显示都发生了什么)

  1. 省略网络相关部分。

  2. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。

  3. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

  4. 创建布局树,并计算元素的布局信息。

  5. 对布局树进行分层,并生成分层树。

  6. 为每个图层生成绘制列表,并将其提交到合成线程。

  7. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。

  8. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。

  9. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

主流程:DOM -》Style-》Layout-》Layer-》Paint

布局

重排(回流)和重绘

通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段(即从上文序号2开始),这个过程就叫重排。

通过 JavaScript 或者 CSS 修改元素没有引起几何位置的变换(即绘制属性(元素的颜色、背景色、边框等)),布局阶段将不会被执行,直接进入了绘制阶段,然后执行之后的一系列子阶段(即跳过序号3,4),这个过程就叫重绘。

影响重排(回流)的操作

  • 添加/删除元素

  • 操作styles

  • display:none

  • offsetLeft,scrollTop,clientWidth

  • 移动元素位置

  • 修改浏览器大小,字体大小。

批量添加DOM时可以使用DocumentFragment

避免Layout thrashing

  • 避免重排(回流)

  • 读写分离:FastDom,原理:读写分离,批量操作,使用window.requestAnimationFrame。

帧的生命周期

减少重绘的方案

  • 利用DevTools识别paint的瓶颈

  • 利用will-change创建新图层

触发合成过程的属性

渲染引擎将跳过布局和绘制,只执行后续的合成操作,这个过程叫做合成。

  • 位置:transform:translate

  • 缩放:transform:scale

  • 旋转:transform:rotate

  • 透明度:opacity

使用Flexbox优化布局

栅格方案性能对比(float vs flex):分别用float 和 flex实现渲染10w个大小一样的div块,然后使用ChromDevTools-Performance进行查看

  • float:rendering(Recalculate+Layout):1804ms Painting:60ms

  • flex:rendering(Recalculate+Layout):1267ms Painting:34ms

flexbox优势:

  • 更高性能的实现方式

  • 容器有能力决定子元素的大小,顺序,对齐,间隔等。

  • 双向布局

高频事件处理

防抖

任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。

比如当监听鼠标移动等事件时:

1
2
3
4
5
6
7
8
9
10
11
12
//用来表示事件是否在执行中。
let enabled =false;

window.addEventListener("pointermove",(e)=>{
if(enabled) return;
enabled = true;
//执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画
window.requestAnimationFrame(()=>{
changeWidth();
enabled = false;
})
})
1
2
3
4
5
6
7
8
9
10
11
12
//节流
let enabled = true;

window.addEventListener("pointermove",(e)=>{
if(enabled){
enabled = false;
window.requestAnimationFrame (()=>{
changeWidth();
})
window.setTimeout(()=>enbled = true,50)
}
})

React时间调度实现

React Fiber:把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

基本原理

  • requestIdleCallback的问题:兼容性不好。在浏览器的空闲时段内调用的函数排队,能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件。

  • 通过rAF模拟rIC:在requestAnimationFrame获取一桢的开始时间,触发一个postMessage,在空闲的时候调用idleTick来完成异步任务。

预渲染

react可以使用react-snap

1
2
3
4
5
6
7
8
9
10
import { hydrate, render } from "react-dom";

const rootElement = document.getElementById("root");
//如果有子节点,表示经历过了ssr
if (rootElement.hasChildNodes()) {
// 通过该方法进行补水
hydrate(<App />, rootElement);
} else {
render(<App />, rootElement);
}

原理:使用 Headless Chrome爬取页面的内容(模拟搜索引擎的爬虫?),然后内容直接放到build的html中。

预渲染的作用

  • 大型单页应用的性能瓶颈:JS下载+解析+执行

  • SSR的主要问题:牺牲TTFB来补救First Paint;实现复杂;

  • Pre-rendering打包时提前渲染页面,没有服务端参与。

可能存在的问题:

  • 内联样式,不经过配置会出现明显的样式闪动。(可以针对首屏把CSS提取出来内嵌到HTML中,剩下的CSS用webpack提取到文件中,进行缓存)

可视化窗口(windowing)提高列表性能 - 虚拟列表

react-window

  • 只渲染可见的行,渲染和滚动的性能都会提升

  • 减少了呈现初始视图和处理更新所需的工作量和时间

  • 避免了 DOM 节点的过度分配,从而减少了内存占用(Lazy loading时,dom会变得非常大)

使用骨架组件减少布局移动(Layout shift)

主要作用就是占位,与要显示的组件大小一样大,等数据加载完成后再显示数据。可以提升用户感知性能

首屏渲染优化方案

首屏 - 用户加载的3个关键时刻

用户加载的3个关键时刻

对应的测量指标:

  • First Contentful Paint(FCP)—– is it happening?(网络请求是否发送出去了?)

  • Largest Contentful Paint(LCP)—– is it useful?(网页内容是否对我有用?)

  • Time To Interactive (TTI) —– is it usable?(是否可以交互了?)

方案:

  • 资源体积太大?资源压缩,传输压缩,代码拆分,Tree shaking,HTTP/2,缓存

  • 首页内容太多?路由/组件/内容懒加载,预渲染/SSR,Inline CSS

  • 加载顺序不合适? prefetch,preload