此名字暂不可见
No distraction,it's you and me

前端笑笑转之性能优化

2025-06-29

一. 引言

​ 性能优化一直前端老生常谈的问题。所以性能是指网站内容在网页浏览器中的加载和渲染速度,以及对用户交互的响应。性能优化,就是让页面有着更快的加载速度,更流畅的用户交互以及更低的资源占用。让用户觉得用的舒服,以便后续继续使用你的产品。

从输入URL到页面加载完成,中间发生了什么

——小林coding

​ 这是一个很重要的问题,因为它非常的重要。

​ 当我们在浏览器的导航栏中输入URL之后,我们需要通过 DNS将 URL 的域名解析为对应的 IP 地址,然后通过这个 IP 地址与服务器建立起 TCP 连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作。

省流:

  1. DNS 解析
  2. TCP 连接
  3. HTTP 请求
  4. 服务端处理请求,HTTP 响应返回
  5. 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户

​ 一般来说,用户输入URL无非就经过上面的几步。因此,要想优化性能,可以从上面五步切入。注意到,前面四步都与网络有关系,而最后一步则是与浏览器有关,从过程上看,分为网络层面和渲染层面。因此,对于性能优化,就可以从这两部分入手。

二. 网络层面

1. DNS缓存

​ DNS在将URL解析为对应的IP地址时,会从根DNS服务器一级一级往下找权威DNS服务器,这个过程不仅耗时,而且还有许多重复性的工作。因此,可以通过DNS缓存在浏览器,操作系统或者本地DNS服务器中缓存URL解析的结果,下次输入相同的URL时就可以直接找到对应的IP地址了。

2. 按需加载

​ 按需加载,又称为懒加载,即根据用户的实际需求来动态加载资源。拿淘宝来举例子,在购物页中有许多的商品图片,如果同时加载的话会导致页面非常卡。因此,可以根据用户的需求来实时加载,当用户的视图到了图片的位置,就加载图片资源,否则不加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: sans-serif;
background-color: #f7f7f7;
}

.image-container {
display: flex;
flex-direction: column;
gap: 20px;
}

.image-placeholder {
width: 100%;
max-width: 600px;
height: 400px;
margin: 0 auto;
background-color: #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #555;
border-radius: 8px;
overflow: hidden;
}

img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
</style>
</head>
<body>
<h2 style="text-align: center;">一个真正想赢的人脸上是没有笑容的</h2>
<div class="image-container" id="imageContainer"></div>
</body>

<script>
const container = document.getElementById('imageContainer')

const imageUrls = Array.from({ length: 2000 }, (v, i) =>
`https://th.bing.com/th/id/OIP.RJBtXDzskdnDONDvtfgesAHaGg?o=7rm=3&rs=1&pid=ImgDetMain&o=7&rm=${i}/600/400`
)

imageUrls.forEach((url, i) => {
const placeholder = document.createElement('div')
placeholder.className = 'image-placeholder'
const img = document.createElement('img')
img.src = url
placeholder.appendChild(img)
container.appendChild(placeholder)
})
</script>

</html>

其中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。目标元素与视口或者指定元素相交时,就会调用观察器的回调函数callback

callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var observer = new IntersectionObserver(callback, option);

// 开始观察
observer.observe(document.getElementById('example'));

// 停止观察
observer.unobserve(element);

// 关闭观察器
observer.disconnect();

//观察多个对象
observer.observe(elementA);
observer.observe(elementB);

3. 浏览器缓存

​ 浏览器缓存,就是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载了,不需要再去服务端请求了。好处在于能减少冗余的数据传输,减少服务端的负担,还能加快客户端加载网页的速度。

​ 浏览器缓存根据优先级分为以下:

​ Service Worker Cache

​ Memory Cache

​ Disk Cache

​ Push Cache

Service Worker Cache

​ Service Worker,是一种独立于主线程之外的JS线程。它脱离于浏览器窗口,因此无法直接访问DOM。它主要用于离线缓存,消息推送和网络代理,它的特点是拦截网络请求,并允许用户自由控制缓存哪些文件。

注:由于Service Worker与拦截请求有关,所以必须适用https来保障安全。

​ Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,它的机制如下:

​ 假设有一个service-worker.js文件,在浏览器执行时就会install一个SW,如果有旧的SW,就会等待旧SW被清除,然后新的SW进入active接管页面。如果页面发起请求,SW就会进入working,先从缓存获取数据,未命中就去按照用户定义的方式拦截请求并缓存,处理完请求后,SW又回到acitve等待下一次请求,直到有新的SWinstall或者长时间没有请求被清除。

Memory Cache

​ Memory Cache,是指存在内存中的缓存。从优先级上说次于Service Worker Cache,但就效率上说,它是响应最快的一种缓存,因为其他再快也快不过内存。它主要存储的是当前页面中已经获取到的资源,例如已下载的样式,脚本,图片等等。

​ 那么Memory Cache,代价是什么呢?

​ 内存缓存虽快,但是它和渲染进程‘生死与共’,当渲染进程结束了,内存中的数据也就不复存在,用人话讲,就是‘命短’

Disk Cache

​ Disk Cache,就是存储在硬盘中的缓存,相较于Memory Cache,Disk Cache的速度会慢一些,但是存储的东西要多得多,时效性也更久。虽然说优先级不是很高,但是它的覆盖面更广,除了时效性更久外,还在于它的适用资源类型广,大部分静态资源,HTML、CSS、JavaScript、图片、字体、视频都可以存储,而且命中范围广,同一个站点可以公用磁盘缓存的资源,还特别主动,HTTP中符合条件的(如 Cache-Control, Expires, ETag, Last-Modified)都会自动缓存

Push Cache

​ Push Cache ,是指 HTTP2 在 server push 阶段存在的缓存。这一段最为神秘,随便找篇文章,它大概会就告诉你:这块的知识比较新,应用也还处于萌芽阶段,我找了好几个网站也没找到一个合适的案例来给大家做具体的介绍。

虽然如此,它还是有一些特点的:

  • Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、Disk Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

除了浏览器缓存类型之外,还有浏览器的两种缓存策略,强制缓存和协商缓存,详情可见小林coding

4.预连接

​ 预连接,即preconnect,它允许浏览器在用户点击链接之前,提前建立于目标服务器地连接,一般用于用户可能点击的链接或者API等,优先级一般

1
<link rel="preconnect" href="https://example.com">

​ 除此之外,还有preload和prefetch。

​ preload用于强制浏览器请求当前页面的一些资源,并将其存储在缓存中,以便在需要的时候能够更快地使用,比较适合于字体,图片,js脚本等关键资源,优先级高

1
2
3
4
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="image.png" as="image">
<link rel="preload" href="script.js" as="script">
<link rel="preload" as="font" type="font/woff2" crossorigin href="font.woff2">

​ 使用必须用as指定属性,比如style代表样式,image图片,crossorigin代表字体。

​ prefetch用于告诉浏览器在空闲时预获取用户将来可能需要使用地资源,一般用于下一页面地资源或者用户可能感兴趣地内容等,优先级低。

​ 在浏览器向服务器请求资源的时候,需要通过DNS解析域名,然后是TCP三次握手和TLS握手,一来一回会浪费不少时间。但如果我已经知道了要和哪个服务器进行通信,不就可以先建立连接,需要资源时直接下载吗?

1
<link rel="preconnect" href="https://b.com">

注:preconnet不宜乱使用,因为浏览器会关闭一段时间未使用的连接,不必要的连接会延迟其他资源。

​ 除了preconnect之外,还有preload和prefetch可以使用。

​ preload可以使资源提前开始加载,在需要用的时候能够更快的使用。比如CSS会阻塞页面的渲染,如果其中有需要下载的字体资源或者CSS文件,那么就可以使用preload提前加载。

1
2
3
4
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="image.png" as="image">
<link rel="preload" href="script.js" as="script">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

注:preload使用必须用as指定属性,比如style代表样式,image图片,crossorigin代表字体。

​ 相较于preload,prefetch更像未来市场,能够加载未来可能用到的资源,让浏览器在空闲的时候去下载,这种下载的优先级比较低,一般用于下一页面地资源或者用户可能感兴趣地内容.

1
<link rel="prefetch" href="https://example.com/nailong"  as="script">

三. 渲染层面

1. 回流和重绘

回流:当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等),这可能影响到页面的整体布局,浏览器便需要重新计算元素的几何属性,然后把计算的结果再次绘制出来。

重绘:当我们修改DOM时导致了样式的变化却未影响到几何属性(比如修改了颜色或背景色),浏览器便会为该元素重新绘制样式。

​ 不难猜到,浏览器的重新绘制是会消耗性能的,不消耗我就不讲,所以必须避免回流和重绘。

​ 而对于回流的重绘的触发,浏览器有自己的优化机制,它会通过Flush队列存储并批量执行来优化回流过程。每当有回流操作,浏览器就会将其放入到队列之中,直到过了一段时间或者操作到了一个阈值才清空队列,但在某些情况下,比如获取布局信息时,因为必须得到最新值,浏览器会不得不清空队列,触发回流重绘来返回最新值。

示例图片

​ 不难看出,重绘不一定回流,但回流一定重绘。而且回流的开销更大,因为它涉及到重新计算元素的几何属性和重新布局整个页面。虽然回流的开销更大,但两个都不是什么善茬,所以在渲染上,要尽可能地减小回流与重绘,比如:

  • 改变元素样式时,最好改变元素的类名
  • 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响
  • 使用CSS动画代替JS动画
  • 使用变量缓存
  • 将DOM离线操作
  • 使用fragment

除此之外,还需要注意一些常见的CSS属性和操作,可能会引起回流和重绘

回流:

​ width、height、padding、margin、border 、display、float、clear、flex 、font-size、line-height、text-align(改变元素几何尺寸的)

​ offsetWidth、offsetHeight、clientWidth、clientHeight(获取元素的宽高属性的)

重绘:

​ color、background-color、border-color、font-weight、text-decoration(改变元素样式的)

2. 使用虚拟列表

​ 虚拟列表突出一个“虚拟”,它的本质跟按需加载差不多,只对可见区域进行渲染,对于非空区域中的数据不渲染或者只渲染一部分内容,从而达到性能优化的目的。

​ 假设现在列表中有10000条数据,而屏幕的可见区域为240px,每条数据的高度为40px,如果再加上缓冲区,那么我们此时只需要渲染12条数据即可。

示例图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>虚拟列表这一块啊</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 40px;
background-color: #f5f5f5;
}

.dropdown-container {
width: 250px;
position: relative;
}

.dropdown-label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}

.dropdown-trigger {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: white;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
position: relative;
}

.dropdown-list {
box-sizing: border-box;
position: relative;
width: 100%;
margin-top: 4px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: white;
max-height: 200px;
overflow-x: hidden;
overflow-y: auto;
display: none;
z-index: 10;
}

.dropdown-list.active {
display: block;
}

.dropdown-item {
padding: 10px 12px;
cursor: pointer;
position: absolute;
width: 100%;
}

.dropdown-item:hover {
background-color: #f0f0f0;
}
</style>
</head>
<body>

<div class="dropdown-container">
<label class="dropdown-label">快!点!</label>
<div class="dropdown-trigger" id="dropdownTrigger">-- 请选择你的英雄 --</div>
<ul class="dropdown-list" id="dropdownList">

</ul>
</div>

<script>
const item_height = 40
const visible = 6
const buffer = 2

const list = document.getElementById('dropdownList')
const trigger = document.getElementById('dropdownTrigger')
const totalItems = 100000

list.style.height = (visible * item_height) + 'px'
list.style.overflowY = 'auto'

function renderVisibleItems(){
const scrollTop = list.scrollTop
const startIndex = Math.max(0,Math.floor(scrollTop / item_height) - buffer)
const endIndex = Math.min(startIndex + visible + buffer, totalItems)


const fragment = document.createDocumentFragment()
for(let i = startIndex;i < endIndex;i++){
const item = document.createElement('li')
item.className = 'dropdown-item'
item.textContent = `鲁雨博${i + 1}号`
item.style.top = (i * item_height) + 'px'
item.addEventListener('click',() => {
trigger.textContent = item.textContent
list.classList.remove('active')
})
list.innerHTML = ''
fragment.appendChild(item)
}
list.appendChild(fragment)
}

list.addEventListener('scroll', () => {
renderVisibleItems();
});

trigger.addEventListener('click',() => {
list.classList.toggle('active')
if (list.classList.contains('active')) {
renderVisibleItems()
}
})

renderVisibleItems()

</script>

</body>
</html>

3. 节流和防抖

​ 节流和防抖本质上都是对于闭包的使用,通过对时间的回调函数进行包裹,以变量来缓存时间信息,最后用setTimeout来控制事件的触发频率。但二者在使用上还是有一些差别的。对于防抖来说,不管触发多少次回调,都只认最后一次,而节流则是触发之后,只认第一次回调。如果要类比,防抖就是坐电梯,节流就是回城。

防抖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function debounce(fn, delay) {
let timer = null

return function (...args) {
let context = this

if(timer) {
clearTimeout(timer)
}
timer = setTimeout(function () {
fn.apply(context, ...args)
}, delay)
}
}

document.addEventListener('scroll', debounce(() => console.log('我滚了'), 1000))

节流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(fn, interval) {
let last = 0

return function (...args) {
let context = this
let now = +new Date()

if (now - last >= interval) {
last = now;
fn.apply(context, ...args);
}
}
}

document.addEventListener('scroll', throttle(() => console.log('我滚了'), 1000))

​ 但是防抖还是存在一些问题的。假如有一个容量无限的电梯,你第一个进去,但是后面不断有人进来,每进来一个就刷新一次等待时间,如果有无穷多的人进来,是我就红温了。用户也是一样,,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。所以,可以通过节流来对防抖进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function throttle(fn, delay) {
let last = 0, timer = null

return function () {
let context = this
let args = arguments
let now = +new Date()

if (now - last < delay) {
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, ...args)
}, delay)
} else {
last = now
fn.apply(context, ...args)
}
}
}

document.addEventListener('scroll', throttle(() => console.log('我滚了'), 1000))

4. CSS优化

​ 对于浏览器来说,只有在布局完成之后才能绘制页面,而布局的前提是要生成渲染树,而渲染树的生成则需要DOM树和CSSOM树的配合。对于用户来说,如果先绘制,等CSS解析完成之后再重绘,体验肯定不好。所以浏览器选择再CSS解析完成之后再绘制页面,看得出CSS对于页面的优化也很重要。一些优化CSS的方法如下:

  • 删除不必要的样式:这看起来很明显,但事实上很多人经常会在CSS中加上一些没必要甚至是无效的CSS

    比如:

    1
    2
    3
    4
    div {
    display: none; //元素会从渲染树中被移除,不会渲染
    ...其他css属性
    }
    1
    2
    3
    4
    5
    div {
    postition: static; //static下,top等定位属性和z-index无效
    top: 10px;
    z-index: 2;
    }
  • 合并CSS

    ​ 可以将多个CSS文件合并为一个文件,以减少HTTP的请求次数,从而提高加载速度。

  • 简化选择器

    ​ 大多数人阅读的习惯是从左往右,但CSS选择器是从右往左进行匹配,假设要找一个id为header的元素,并将样式应用到子元素a上,浏览器会遍历页面中所有的a元素并且确定器父元素的id是否为header。所以优化选择器的原则是尽量避免使用消耗更多匹配时间的选择器,比如减少 * 通配符的使用。

    CSS选择器的效率如下:

    1.id选择器
    2.类选择器
    3.标签选择器
    4.相邻选择器
    5.子选择器
    6.后代选择器
    7.通配符选择器
    8.属性选择器
    9.伪类选择器

  • 预加载重要资源

    ​ 正如上面所说,对于一些比较重要的样式,可以使用rel = ’preload‘将元素转换为预加载器,用来提交加载一些字体,图片

    1
    2
    3
    4
    <link rel="preload" href="style.css" as="style">
    <link rel="preload" href="image.png" as="image">
    <link rel="preload" href="script.js" as="script">
    <link rel="preload" as="font" type="font/woff2" crossorigin href="font.woff2">
  • CSS属性排序:

    ​ 之前在网上看到一篇文章,里面提到了CSS属性的顺序书写,觉得蛮有用的,可以避免书写某些重复的CSS属性,这里引用以下:

    涉及几何属性与外观属性,结合盒模型与从外到里的结构排序属性。综合太极图的哲学思想,将一些回流的几何属性排在最前面,毕竟这些属性决定了节点的布局、尺寸等与本质有关的状态,有了这些状态才能派生出节点更多的外观属性,逐一组成完整的节点。好比一座摩天大楼的构筑过程,从打桩(存在)、搭设(布局)、主体(尺寸)、砌体(界面)、装修(文字)、装潢(交互)到验收(生成一个完整的节点),每步都基于前一步作为基础才能继续下去。

    [‬⁠‬‌‌‍⁠‬‌‬⁠‬‌‌‬‍⁠‬‍‬稀土掘金](玩转 CSS 的艺术之美 - JowayYoung - 掘金小册)

​ 总结就是按布局 → 尺寸 → 界面 → 文字 → 交互的方式顺序定义,从元素的存在,再到元素的大小,再到界面,再到内部文字,最后才是用户交互。

Author: John Doe

Link: https://159357254680.github.io/2025/06/29/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%C2%B7/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Webpack
NextPost >
zustand
CATALOG
  1. 1. 一. 引言
  2. 2. 二. 网络层面
    1. 2.1. 1. DNS缓存
    2. 2.2. 2. 按需加载
    3. 2.3. 3. 浏览器缓存
      1. 2.3.1. Service Worker Cache
      2. 2.3.2. Memory Cache
      3. 2.3.3. Disk Cache
      4. 2.3.4. Push Cache
    4. 2.4. 4.预连接
  3. 3. 三. 渲染层面
    1. 3.1. 1. 回流和重绘
    2. 3.2. 2. 使用虚拟列表
    3. 3.3. 3. 节流和防抖
    4. 3.4. 4. CSS优化