性能优化是个老生常谈的话题,也是面试中的老熟题。本文从客户端渲染的层面介绍浏览器渲染过程中可以优化的地方。
浏览器渲染过程
浏览器的渲染过程需要通过渲染引擎去解析网页代码,并将网页显示在屏幕上。常见的渲染引擎有Chrome浏览器使用的Blink, Firefox浏览器使用的Gecko等。
渲染引擎主要包含几个模块:HTML解析器、CSS解析器、布局和绘图等。这些模块共同工作,最终把网页呈现给用户。
具体来说,渲染过程可以分几个步骤:
-
解析
HTML
、解析CSS
浏览器从服务器接收文档后,通过
HTML
解析器解析HTML
文档,并构建一个文档对象模型(DOM
)树。解析HTML
过程中,当遇到<link>
标签或<style>
标签时,浏览器会同时开始解析CSS
。解析
HTML
和解析CSS
是两个并行的过程。CSS
的解析不会阻塞DOM
树的构建。虽然不会阻塞DOM树的构建,但是它是阻塞渲染的,渲染引擎必须等到CSSOM树构建完成才能进行渲染。 -
构建渲染树
浏览器将 DOM 树和 CSSOM 树结合起来构建一个渲染树(
Render Tree
)。浏览器检查每个节点,从 DOM 树的根节点开始,以确定哪些元素是可见的,以及它们的相应 CSS 规则。渲染树包含需要显示的节点和每个节点的样式信息。 -
布局(Layout/Reflow)
计算元素位置。渲染树构建完成,浏览器进入布局阶段。在这个阶段,浏览器会计算渲染树中每个对象的确切位置和大小。
-
绘制图层
渲染像素。根据渲染树和布局信息,浏览器接着绘制页面的每个可视元素,将元素的各个部分转换为屏幕上的像素。
此外,浏览器自身也做了一些优化。逐步渲染。为了提高用户体验,现代浏览器通常采用"解析一部分,渲染一部分"的策略,即增量渲染。这允许用户尽快看到页面的主要内容,即使页面的其他部分仍在加载和解析中。
CSS优化技巧
1、给DOM中每个元素节点添加样式的时候,浏览器会通过CSS引擎去查询CSS样式表,然后应用到这个元素上。因此我们要尽量的减少查表花费的时间,让它尽快找到对应元素的样式。而浏览器匹配CSS规则的时候是从右向左的,如
css
.box p span{
font-size:20px;
}
浏览器会先去查找所有span标签对应的元素,再去找父元素是p的span,再去找父元素是.box
的p,才能配到对应的元素。这个过程就遍历了很多无用的元素。所以呢,减少无用的遍历就是一个看似很小实则很重要的一个优化点。那要怎么做呢?
- 减少通配符(* )的使用。(浏览器会遍历所有节点)
- 尽量少用标签选择器。使用高效选择器。
- 减少嵌套。
- 除无用CSS规则,压缩和精简CSS代码,减少文件大小。
上面提到过,CSS会阻塞渲染 ,这是为了确保渲染内容之前有完整的样式信息。因此要让CSS资源尽早尽快的下载下来。所以就有了以下的优化:
2、把CSS资源放到<Head>
里,确保它们能够优先加载,减少页面阻塞渲染的时间。
3、把CSS、JS、图片等一些静态资源放到CDN上,减少资源的下载时间。
JavaScript的阻塞行为
JS 能够操作和修改网页的内容,包括文档对象模型(DOM)和CSS对象模型(CSSOM)。因此,当浏览器执行 JS 代码时,它会暂停构建 CSSOM 和 DOM,以便 JS 可以对它们进行修改。 浏览器在渲染过程中如果遇到了script
标签,会暂停文档的解析,将控制权交给JS引擎 。去下载JS并在JS引擎中执行JS代码,JS代码执行完毕后,再将控制权交回给渲染引擎 继续解析文档。
为了避免阻塞,可以将 JS 加载变成异步操作,使用 async
或 defer
属性:
async :JS下载完成后脚本会立即执行,不等待文档解析完成。
defer:脚本下载完成后,会推迟执行,等文档解析完成后再去执行。
减少DOM
操作
渲染引擎 和JS引擎 是相互独立的。它们之间通过桥接接口进行切换。桥接接口将 JS 对 DOM 的操作转发给渲染引擎,然后渲染引擎将更新反馈回JS,而这个在JS引擎和渲染引擎来回切换的过程是有一定开销的。要减少这部分的开销就要减少DOM操作。
此外,DOM 的修改可能触发重绘 (Repaint)或回流(Reflow):
重绘 :当页面中某些元素的样式发生变化,但是不会影响到布局,浏览器重新绘制修改的部分,这个过程就是重绘。
回流 :当渲染树中部分或者全部元素的布局 或几何尺寸 (宽高显隐等)发生变化时,浏览器会重新计算全部或部分元素的几何属性,并进行重新绘制。它涉及到重新计算布局,会消耗计算资源,要比重绘的代价更为昂贵。回流总是伴随着重绘,重绘不一定会引起回流。
重绘和回流都会消耗资源,影响页面性能,因此要尽量减少重绘和回流。
那么问题来了,如何减少DOM操作呢?
1、 DoucmentFragment :允许我们像操作真实DOM一样去调用DOM API,但它不是真实DOM树的一部分,不会触发DOM树的重新渲染。
MDN例子:DoucmentFragment
会将所有的DOM操作缓存起来,最后添加到真实DOM中。
html
<ul id="list"></ul>
js
const list = document.querySelector("#list");
const fruits = ["Apple", "Orange", "Banana", "Melon"];
const fragment = new DocumentFragment();
fruits.forEach((fruit) => {
const li = document.createElement("li");
li.textContent = fruit;
fragment.appendChild(li);
});
list.appendChild(fragment);
2、 event loop
JavaScript
是单线程运行的,这意味着在执行代码时,它一次只能处理一个任务。当遇到耗时操作时,比如网络请求或文件读取,传统的同步操作会导致页面堵塞,用户体验下降。
为了解决这个问题,JavaScript
引入了异步事件的概念。通过回调函数、Promise等,允许代码执行不阻塞后续操作,而是在完成时触发相关逻辑。这样,即使有耗时操作,页面也能保持响应。
为了管理这些异步任务,JavaScript
使用了事件循环机制。其工作流程如下:
- 初始化时调用栈为空。全局执行上下文的
script
作为第一个宏任务进入宏任务队列。。 script
作为当前运行任务进入调用栈执行,遇到异步时将其回调存入微任务队列或宏任务队列。- 当前执行栈中的事件执行完毕后,当前
script
脚本被弹出宏任务队列。开始处理微任务队列。 - 检查微任务队列并将微任务回调压入调用栈执行。注意:每一次事件循环要将微任务队列清空,再去执行下一步。
- 微任务队列清空后,执行渲染操作,更新页面。
- 检查是否有web worker,有就去执行。
- 取出下一个宏任务,压入调用栈,循环往复。
vue
中nexttick
就是基于event loop的一个应用。它将DOM更新操作封装成微任务,在当前任务结束后批量执行,减少渲染次数,优化性能。
3、减少重绘和回流
思路和上面两个方法类似,也是等多个会引起重绘和回流的操作都执行完毕后再去触发重新渲染。具体操作有以下几种:
- 少使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局。
- 避免频繁读取布局信息,获取一次后将其缓存,再次使用时直接取缓存。可以通过闭包或者保存到全局变量中保存某个变量。
- 修改多个样式时,可以采用添加类名的方式,而不是逐个去修改。如:
js
// 避免逐条修改
const box = document.getElementById('box')
box.style.width = '300px' ;
box.style.height = '300px';
box.style.color = '300px';
// 通过添加类名修改
box.classList.add('active')
css
.active:{
width:300;
height:300;
color:orange;
}
- 将元素设置
display:none
,操作结束后再显示出来。 - 浏览器自身对页面的回流和重绘通过渲染队列进行了优化。浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
4、动画
动画会频繁的操作DOM,可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流。这样,元素的位置不再影响周围元素的布局,其他元素不会在考虑这个脱离文档流的元素的存在。不会因为动画导致其他元素的重绘。
一些常用的动画格式:gif json+lottie apng cavnas。根据业务场景选择合适的动画格式。
一些常用的优化策略
1、防抖和节流
防抖:是指事件被触发n秒后在执行回调函数。如果在这n秒内再次被触发,则重新计时。可用在提交表单时防止多次点击按钮发送请求多次。代码如下:
js
dunction debounce(fn, delay){
let timer = null;
return function () {
let context = this;
let args = arguments
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(context,args)
}, delay);
};
};
节流:在一个规定的时间内,触发的回调函数只执行一次。如监听scroll事件,减少事件调用频率,还可用在拖拽、resize等频繁触发事件的场景中。
js
function hrottle(callback, delay) {
var lastTime = 0;
return function() {
var currentTime = new Date().getTime();
if (currentTime - lastTime > delay) {
callback.apply(this, arguments);
lastTime = currentTime;
}
};
}
2、按需加载
异步组件
vue
中引入了异步组件的APIdefineAsyncComponent
。它可以推迟组件加载,在需要的时候再去动态引入,比如滚动到可视区域时,再去加载这个组件。可以优化首屏加载速度。
js
import { defineAsyncComponent } from 'vue'
// 在需要时引入组件
if(xxx) {
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
}
懒加载
对于一些图片比较多的网站,使用图片懒加载可以优化首屏加载速度。只加载首屏的图片,可视区域外的图片在滚动到可视区域内再去加载。
总结
以上的优化方案只是浏览器从开始解析文档到页面渲染过程中的优化措施,其他还有网络层面、缓存层面的优化并未涉及。
以上的过程涉及到很多常考的面试题。可以自己总结一下。
1、性能优化
2、浏览器渲染流程
3、重绘和回流
4、事件循环机制
5、防抖和节流
参考:
MDN
修言大佬的小册