浏览器渲染流程、优化技巧
浏览器渲染流程
-
🌲 解析HTML生成DOM树。
-
🌲 解析CSS生成CSSOM树。
-
🌲 DOM树、CSSOM树合并生成渲染树(Render Tree)。
-
🧙♂️ 回流(Reflow)。
-
👨🎨 重绘(Repaint)。
-
😎 GPU绘制页面。
渲染树 (Render Tree)
💡 渲染树的生成,对应的是浏览器渲染的1-3步骤(1.HTML、2.CSS、3.Render Tree)。
-
从DOM树根节点循环遍历每一个可见节点。
🚨 不可见节点:
script
、meta
、link
等 -
对于每个可见节点,找到CSSOM树中对应规则运用。
🚨 不可见属性:
display:none
-
根据可见节点和样式,生成Render树。
- 最终,渲染树只包含可见节点。
回流 (Reflow )
💡 回流,是浏览器计算各种元素在设备视口的位置和大小,这个阶段也是最消耗资源的阶段。
<!-- 例:--> <!-- 例如以下HTML代码,父div标签,宽度为50%,子div标签为父div标签的50%。--> <!-- 回流阶段就会根据视口的宽度,将各个元素的50%,转换为实际的像素长度。--> <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">什么是回流?</div> </div> </body> </html>
导致回流的原因:
-
🚨 页面初始化
-
🚨 添加、删除DOM元素
-
🚨 元素位置变化
-
🚨 元素尺寸变化
-
🚨 元素内容变化 (包括text文本、图片变化)
-
🚨 浏览器窗口大小变化
重绘 (Repaint)
经过回流阶段,已经能确定Render树结构、以及元素对应的位置、样式。
重绘阶段,一些元素需要更新属性,而这些属性只是影响元素的外观,而不会影响布局的,比如background-color,则称之为重绘。
如何减少回流、重绘
💡 回流会导致额外的资源损耗,浏览器通过队列化批量执行来优化重绘过程。通常是将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才执行并清空队列(除强制回流API)。以下罗列一些常见的优化技巧。
(1)🙋♂️ 减少使用强制回流API
以下API尽量少用、不用,否则会导致强制回流。
-
offsetTop、offsetLeft、offsetWidth、offsetHeight
-
scrollTop、scrollLeft、scrollWidth、scrollHeight
-
window.scrollX、window.scrollY
-
element.focus()
-
mouseEvt.offsetX、mouseEvt.offsetY
-
window.getComputedStyle()
-
......
(2)🙋♂️ 合并样式操作
同时对元素进行多次操作,会导致几何结构变化,引起回流。
🐢 优化前 回流多次
// 老版本浏览器(未优化)没有批量操作队列,会触发三次回流 const el = document.getElementById('test'); el.style.padding = '5px'; // 🐢 第一次操作,回流 el.style.borderLeft = '1px'; // 🐢 第二次操作,回流 el.style.borderRight = '2px'; // 🐢 第三次操作,回流
🐇 优化方案(2.1)
// cssText 通过cssText属性改变样式 回流一次 const el = document.getElementById('test'); el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
🐇 优化方案(2.2)
// class 直接改class 回流一次 const el = document.getElementById('test'); el.className += ' active';
(3)🙋♂️ 批量操作DOM
当需要对DOM进行一系列修改时,可以通过以下步骤进行优化:
- ☝️脱离文档流 (引起回流)
- ☝️对元素进行批量操作
- ☝️将元素带回文档流 (引起回流)
🐢 优化前 回流多次
// 该操作会导致浏览器多次“回流” const ul = document.getElementById('ulElement'); // 循环添加5次li元素 for (let i = 0; i < 5; i++) { const li = document.createElement("li"); li.textContent = "text"; ul.appendChild(li); };
🐇 优化方案(3.1)
// 🛠 先隐藏目标元素,完成操作后在显示,该方案固定回流两次 function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement("li"); li.textContent = "text"; appendToElement.appendChild(li); } } const ul = document.getElementById("list"); ul.style.display = "none"; // 1.先隐藏ul元素 回流+1 appendDataToElement(ul, data); // 2.ul元素添加多个li ul.style.display = "block"; // 3.添加完成后 在显示 回流+1
🐇 优化方案(3.2)
// 🛠 使用文档片段,在DOM外创建一个子树,在拷贝回原文档 const ul = document.getElementById("list"); const fragment = document.createDocumentFragment(); // 当前DOM外创建一个子树🌲 appendDataToElement(fragment, data); // 循环添加 ul.appendChild(fragment); // copy回原文档 回流+1
🐇 优化方案(3.3)
// 🛠 拷贝原元素,操作拷贝元素,在执行替换,性能较差 const ul = document.getElementById("list"); const clone = ul.cloneNode(true); // copy元素 appendDataToElement(clone, data); // 循环添加 ul.parentNode.replaceChild(clone, ul); // 替换 回流+1
(4)🙋♂️ 复杂动画脱离文档流
// 脱离文档流 使其与其他元素互不联系 function update() { const box = document.getElementById("box"); box.style.position = "absolute"; }
(5)🙋♂️ CSS3 硬件加速
🔍 避免回流的硬件加速css属性:
- transform
- opacity
- filters
- Will-change
👀 缺点
- 内存占用量较大
- 在GPU渲染字体会导致抗锯齿无效