文章目录
关键渲染路径
当页面输入URL,到页面加载并渲染,需要经过几个关键步骤
- DNS解析:将域名解析为ip地址
- TCP连接: 三次握手
- 服务端接收请求,返回数据,客户端接收请求,解析数据开启
关键渲染路径
1 解析html,生成DOM Tree
对HTML文档进行解析,并在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>标题</title>
<style>
body {
font-size: 20px;
}
div {
width: 100px;
height: 100px;
}
span {
color: #000;
}
p {
display: none
}
</style>
</head>
<body>
<div class="box1">
<span>span-box1</span>
</div>
<div class="box2">
<span>span-box2</span>
</div>
<p>---p---</p>
</body>
</html>
2 CSS 解析,生成CSSOM 树
3 DOM与CSSOM 生成 Render Tree
- step1: 从 DOM 树的根节点开始遍历,筛选出所有可见的节点;
- step2:仅针对可见节点,为其匹配 CSSOM 中的 CSS 规则;
- step3:发射可见节点(连同其内容和计算的样式)。
遇到<script>会暂停渲染,有限执行js,执行结束后继续生成Render Tree
4 布局、绘制
浏览器对渲染树进行遍历,将元素间嵌套关系以盒子模型的形式写入文档流。
盒模型在布局过程中会计算出元素确切的大小和定位。计算完毕后,相应的信息被写回渲染树上,就形成了“布局渲染树”。同时,每一个元素盒子也都携带着自身的样式信息,作为后续绘制的依据。
渲染优化——减少重排/重绘
浏览器渲染优化可以从很多角度考虑,核心思路只有一个:减少页面重排/重绘的次数或者数量,浏览器随时都会发生更新,以下基本操作会导致页面重新渲染。
重排必定重绘,重绘不一定必须要重排
重排(Reflow)
- 改变元素的位置或尺寸。
- 添加或删除可见的 DOM 元素。
- 改变元素的字体大小、行高、内容或其他影响布局的属性。
- 计算或获取元素的 offsetWidth、offsetHeight、scrollTop 或 scrollLeft 等属性。
- 使用 float、position 或 display 等属性改变元素的显示方式。
- 当父元素的宽高发生变化时,子元素也会触发重排。
- 使用表格布局时,某些操作可能会导致整个表格的重排。
重绘(Repaint)
- 改变元素的颜色、背景、边框或阴影等非几何属性。
- 给元素添加或删除一个类名,导致其样式发生变化。
- 使用 CSS 动画或过渡时,浏览器需要重新绘制元素。
- 当元素的内容发生变化时,浏览器需要重新绘制元素。
1 CSS优化
1-1 减少JS动态修改样式
transform可以借用GPU对动画进行处理,省去布局和重绘。
使用javascript动态修改样式会引起频繁的重排/重绘,需要谨慎使用:
let Div = document.getElementsById('div');
Div.style.width = '100px'; // ⚠️
同时尽量避免在循环中修改元素的样式:
let imgDiv = document.getElementsByClassName('img')
for(let i = 0; i<imgDiv.length; i++) {
imgDiv[i].style.width = 100 + 'px'
imgDiv[i].style.height = 100 + 'px'
} // ⚠️
1-2 使用 transform 和 opacity
transform(动画)可以opacity(透明)浏览器会借助GPU进行处理,省去布局和重绘。
使用javascript动态修改样式会引起频繁的重排/重绘,需要谨慎使用:
let Div = document.getElementsById('div');
Div.style.width = '100px'; // ⚠️
1-3 contain、will-change
- contain
contain 属性支持许多值,如 none、strict、content 、layout以及这些值的组合(例如 contain: layout paint),每个值都提供了不同程度的隔离。选择哪个值取决于你的具体需求和期望的隔离级别。
使用javascript动态修改样式会引起频繁的重排/重绘,需要谨慎使用:
.example {
contain: layout paint;
}
- 当为类名为example的元素设置如上属性时,该元素及其子元素将不会影响外部的布局与绘制,同时也不会被外部的布局绘制所影响。在某些情况下,特别是对于大型或复杂的页面布局,这种隔离可以帮助减少重排(reflow)和重绘(repaint)的次数。
在使用 contain属性时,应该仔细考虑其对页面布局和性能的具体影响,过度使用或不当使用可能会导致性能问题
- will-change
当你知道一个元素即将被动画或过渡效果修改时,可以使用will-change属性提前告知浏览器。这样,浏览器可以针对这些属性进行优化,从而提高动画的流畅性和性能。
对于一个即将进行缩放和透明度变化的元素,可以这样设置:
.element {
will-change: transform, opacity;
transition: transform 0.5s, opacity 0.5s;
}
.element:hover {
transform: scale(1.5);
opacity: 0.5;
}
will-change属性应该只在确实需要优化的场景中使用。对于简单的动画或变化,使用will-change可能并不会带来明显的性能提升。大量使用可能会造成反效果
1-4 减少table布局的使用
2 优化DOM操作
2-1 使用 documentFragment / display: none 来添加 / 删除多个元素
- 添加多个元素
Ïconst list = document.getElementById('list')
// ===========创建一个文档片段,此时还没有插入到 DOM 结构中⭐==========
const frag = document.createDocumentFragment()
for (let i = 0; i < 20; i++) {
const li = document.createElement('li')
li.innerHTML = `List item ${i}`
// 先插入文档片段中
frag.appendChild(li)
}
// 都完成之后,再统一插入到 DOM 结构中
list.appendChild(frag)
- 删除多个元素
将元素的 display 属性设置为 none 会使元素从文档流中完全移除,这意味着它不仅在视觉上不可见,而且不再占用任何空间。
- 由于 display: none 隐藏的元素不再参与布局,因此任何与该元素相关的布局变化都不会触发重排。此外,如果该元素之前已经在页面上绘制过,隐藏它也不会触发重绘(因为它已经从 DOM 树中移除,不再需要渲染)。
- 重新显示元素:当将 display 属性从 none 改回其他值(如 block)时,元素会重新进入文档流,这通常会触发一次重排和重绘,因为浏览器需要重新计算布局并绘制该元素。
- 尽量将布局变动限制在较小的 DOM 子树内,避免全局性的布局变化。
因为元素隐藏后有可能导致其他元素的重排重绘
2-2 为查询做缓存
const pList = document.getElementsByTagName('p')
const length = pList.length // ✅ 缓存length,只进行一次查询
for (let i=0;i<length;i++){
// xxx
}
const pList = document.getElementsByTagName('p')
for (let i=0;i<pList.length;i++){ // ⚠️ 没有缓存length,查询多次
// xxx
}
2-3 事件代理减少开销
3 HTML加载优化
3-1 减少iframe使用
iframe的加载会阻塞父文档的加载
只有当iframe加载完成时,才会触发父文档的onload事件- 相较于原文档,iframe上元素的创建消耗会更大
当使用iframe时,可以先加载空白iframe,随后设置内容,减少父文档load时间
<iframe id='iframe'></iframe>
document.addEventListener('DOMContentLoaded', function() {
// DOM 完全加载和解析完成后的代码
document.getElementById('iframe'),setAttribute('src', 'xxx.example.com')
})
3-2 js位置
- js的执行会阻塞页面的渲染,因此将js放在body的最下面
- 使用js时,尽量使用外链,会有助于提高加载速度
<script src="defer.js" defer></script>
// defer: 告诉浏览器立即下载脚本,但延迟执行直到文档完全解析和显示。适用于不依赖于文档中其他脚本的脚本。
<script src="async.js" async></script>
// async: 告诉浏览器异步下载脚本,下载完成后立即执行,不保证执行顺序。适用于可以独立运行的脚本。
3-2 减少html元素的嵌套层级
4 节流、防抖、rAF
4-1 节流
原理:
- 节流的核心原理是在规定的时间间隔内只允许某个函数执行一次。
- 当事件首次触发时,会立即执行处理函数,然后在指定的时间间隔内忽略后续的事件触发,直到时间间隔结束后再允许执行下一次事件处理。
使用场景:
- 页面滚动:限制滚动事件的触发频率,避免频繁触发导致的性能问题。
- 窗口大小调整:减少resize事件的触发次数,避免频繁计算。
- 按钮点击:防止按钮在短时间内被多次点击,造成重复提交或多次触发。
onst div1 = document.getElementById('div1')
//==========节流===========
function throttle(fn, delay = 100) {
let timer = null
return function () {
if (timer) { // 连续触发时,有仍在执行的timer则停止
return
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
div1.addEventListener('drag', throttle(function (e) {
console.log(e.offsetX, e.offsetY)
}))
4-2 防抖
原理:
- 防抖的核心原理是在事件被触发后等待一段时间,如果在这段时间内再次触发了相同的事件,则重新计时。
- 只有当事件触发后的一段时间内没有再次触发时,才执行相应的操作。
使用场景:
- 搜索框输入:在用户停止输入一段时间后再发送搜索请求,避免频繁的请求。
- 表单验证:在用户停止输入后再进行表单验证,减少不必要的验证请求。
const input1 = document.getElementById('input1')
// ========防抖===========
function debounce(fn, delay = 500) {
// timer 是闭包中的
let timer = null
return function () {
if (timer) { //连续输入的时候,中止上一个输入的timer
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
input1.addEventListener('keyup', debounce(function () {
console.log(input1.value)
}, 600))