✍️ Tangxt | ⏳ 2020-06-25 | 🏷️ 浏览器底层渲染机制 |
background-color
,那么就会发生「重绘」 -> 上色为啥要讲这俩东西?
因为我们写的 JS 代码,很有可能会导致回流多次,重绘多次
而回流和重绘这俩东西特别费时间,而这意味着页面效果就不好了 -> 所以我们要尽可能的避免回流和重绘
元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染
如:
元素样式的改变(但宽高、大小、位置等不变)
如:
outline
visibility
color
background-color
我们是如何用 jQuery 或者原生 JS 操作 DOM?
很简单,获取元素 -> CRUD DOM 元素
可假如我们一个大页面有 3 个板块——A、B、C,我们需要改 A、B、C 这 3 个板块,我们在写代码的时候,为了板块分离,所以把板块 A 写在一个闭包里边,板块 B 写在另外一个闭包里边……
当我们操作 DOM 的时候,每个板块都会被操作一次,这样就得操作 3 次了……
而现在我们基于 Vue/React 这种数据渲染式,只要一次即可搞定
数据渲染时代,代码也是分模块开发的,但是它们渲染的时候,是拿到最新数据,重新生成虚拟 DOM,然后拿新的虚拟 DOM 跟之前旧的统一对比一下,这样一来哪个版块改了,哪个没改,就统一渲染一次就好了!
由于有虚拟 DOM、DOM diff 机制,是数据驱动视图的,所以我们不需要操作 DOM
所以一定可以避免我们回流的次数,即旧姿势需要多次回流才能处理完成,而新姿势,统一一次回流就能处理完成了
总之,新姿势引发回流的次数一定要比我们使用 jQuery、原生 JS 要少得多,毕竟新姿势是统一处理的!而不是 A 板块处理后,视图更新一次,然后 B 板块处理,视图再更新一次……
当然,如果我们自己就是想操作 DOM 呢?就是想用 jQuery 和原生 JS 来操作 DOM 呢?
那么我们最好就是读写分离!
话说,啥是「读写分离」呢?
如果你这样写:
let btn = document.getElementById("btn");
btn.style.width = "100px";
btn.style.height = "200px";
那么在老的浏览器里边会触发两次,但是现代浏览器的渲染队列机制,会检测下一行代码是否也是「回流或重绘」操作,如果是,那就先不渲染,先排队先,直到不是了,才会去渲染
所以这会引发一次回流
可如果你这样呢?
let btn = document.getElementById("btn");
btn.style.width = "100px";
console.log(btn.clientWidth)
btn.style.height = "200px";
那就会触发两次了,因为 btn.clientWidth
是个读操作,所以它得读 btn
最新的 clientWidth
值,因此, btn.style.width = "100px"
执行了就会去渲染,然后读一次,然后 btn.style.height = "200px"
又会去渲染一次……
所以我们得这样写:
let btn = document.getElementById("btn");
btn.style.width = "100px";
btn.style.height = "200px";
console.log(btn.clientWidth)
这中写法就是「读写分离」了 -> 最终只会引发一次回流
那么有哪些属性属于读操作呢?(这些属性都会让浏览器立即渲染,毕竟都是要获取最新样式的)
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、getComputedStyle
、currentStyle
如果出现以上这些属性的读操作都会 刷新渲染队列,即都会引发浏览器立即重新渲染
我们改某个元素的样式时,一般不会只改一个,所以我们可以统一集中处理一下:
div.style.cssText = 'width:20px;height:20px;'
div.className = 'box';
因此,不用写成这个样子:
btn.style.width = "100px";
btn.style.height = "200px";
如果你这样写:
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
即,读写都在一起了,上边的代码会产生两次回流
但如果你改成这样:
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
即,把获取的值用个变量存起来,3、4 行代码在获取值的时候拿到的都是存储的那个变量值,那么在渲染时,它们都是排在渲染队列里边,然后再一起渲染!
这样只会产生一次回流
其原理还是「读写分离」
假如你有这样一个需求,那就是需要往某个元素里边添加 10 个span
元素,如果你这样做:
for (let i = 0; i < 10; i++) {
let span = document.createElement("span");
navBox.appendChild(span);
}
那么会产生 10 次回流
可如果你这样做:
let frag = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
let span = document.createElement("span");
frag.appendChild(span);
}
navBox.appendChild(frag);
那么只会产生一次回流
而产生一次回流的姿势,还有这种字符串拼接姿势:
let str = ``;
for (let i = 0; i < 10; i++) {
str += `<span></span>`;
}
navBox.innerHTML = str;
元素脱离文档流,意味着脱离了一个二维平面,但还是会触发回流,只是这种回流计算要比那种非脱离的要简单得多!
如你的动画改了元素的 margin 属性,元素是脱离的性能要比非脱离的好!
比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘;
transform、opacity、filters……等这些属性会触发硬件加速,不会引发回流和重绘……
但这可能会引发的坑有:过多使用会占用大量内存,性能消耗严重、有时候会导致字体模糊等
毕竟这是用机器性能来提供能量呀,即通过烧钱来换取性能呀!
每次 1 像素移动一个动画,但是如果此动画使用了 100%的 CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。每次移动 3 像素可能看起来平滑度低了,但它不会导致 CPU 在较慢的机器中抖动
举例来说:
你要从 1px 移动到 100px,如果 1px、1px 这样移动的话,显然效果感人,但是会回流 100 次,但如果你 10px 移动一次呢?只需要回流 10 次,但这样一来,动画效果就不好看了
所以我们一般在做动画的时候,都是用「CSS3 动画+transform
」来搞的!而不是用 JS 来搞动画!如果这动画非得用 JS 才能实现的话,那么你可以用requestAnimationFrame
来搞!
这个不怎么用了!
➹:页面重排与重绘(Reflow & Repaint) - 知乎
➹:Web 性能优化-页面重绘和回流(重排) - lizhen’s blog
➹:浏览器渲染页面过程与页面优化 - 前端小事 - SegmentFault 思否
transform
改的,那就不用其它方式渲染 = “生成布局”(flow)+ “绘制”(paint) -> 这相当于是在生成一个模板;最后一步,Display -> 相当于是把模板印到页面上!。形象点来说,就是一个枚印章,沾了点墨水,然后印到纸张上!
display:none
会触发 reflow,而 visibility:hidden
只会触发 repaint,因为没有发生位置变化。可以把页面当作是一张纸,把元素当作是一个个有大有小的字体模具,如这样:
layout:
repaint:
点阵(像素化):
GPU 渲染呈现:
想想一个字体模具突然变大了,或者移动位置了,那么你想这会对其它字体模具有多么大的影响啊!