一. 引言
性能优化一直前端老生常谈的问题。所以性能是指网站内容在网页浏览器中的加载和渲染速度,以及对用户交互的响应。性能优化,就是让页面有着更快的加载速度,更流畅的用户交互以及更低的资源占用。让用户觉得用的舒服,以便后续继续使用你的产品。
从输入URL到页面加载完成,中间发生了什么
——小林coding
这是一个很重要的问题,因为它非常的重要。
当我们在浏览器的导航栏中输入URL之后,我们需要通过 DNS将 URL 的域名解析为对应的 IP 地址,然后通过这个 IP 地址与服务器建立起 TCP 连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作。
省流:
- DNS 解析
- TCP 连接
- HTTP 请求
- 服务端处理请求,HTTP 响应返回
- 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户
一般来说,用户输入URL无非就经过上面的几步。因此,要想优化性能,可以从上面五步切入。注意到,前面四步都与网络有关系,而最后一步则是与浏览器有关,从过程上看,分为网络层面和渲染层面。因此,对于性能优化,就可以从这两部分入手。
二. 网络层面
1. DNS缓存
DNS在将URL解析为对应的IP地址时,会从根DNS服务器一级一级往下找权威DNS服务器,这个过程不仅耗时,而且还有许多重复性的工作。因此,可以通过DNS缓存在浏览器,操作系统或者本地DNS服务器中缓存URL解析的结果,下次输入相同的URL时就可以直接找到对应的IP地址了。
2. 按需加载
按需加载,又称为懒加载,即根据用户的实际需求来动态加载资源。拿淘宝来举例子,在购物页中有许多的商品图片,如果同时加载的话会导致页面非常卡。因此,可以根据用户的需求来实时加载,当用户的视图到了图片的位置,就加载图片资源,否则不加载。
1 |
|
其中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。目标元素与视口或者指定元素相交时,就会调用观察器的回调函数callback
。
callback
一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
构造函数的返回值是一个观察器实例。实例的observe
方法可以指定观察哪个 DOM 节点。
1 | var observer = new IntersectionObserver(callback, option); |
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 | <link rel="preload" href="style.css" as="style"> |
使用必须用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 | <link rel="preload" href="style.css" as="style"> |
注: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 |
|
3. 节流和防抖
节流和防抖本质上都是对于闭包的使用,通过对时间的回调函数进行包裹,以变量来缓存时间信息,最后用setTimeout来控制事件的触发频率。但二者在使用上还是有一些差别的。对于防抖来说,不管触发多少次回调,都只认最后一次,而节流则是触发之后,只认第一次回调。如果要类比,防抖就是坐电梯,节流就是回城。
防抖:
1 | function debounce(fn, delay) { |
节流:
1 | function throttle(fn, interval) { |
但是防抖还是存在一些问题的。假如有一个容量无限的电梯,你第一个进去,但是后面不断有人进来,每进来一个就刷新一次等待时间,如果有无穷多的人进来,是我就红温了。用户也是一样,,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。所以,可以通过节流来对防抖进行优化:
1 | function throttle(fn, delay) { |
4. CSS优化
对于浏览器来说,只有在布局完成之后才能绘制页面,而布局的前提是要生成渲染树,而渲染树的生成则需要DOM树和CSSOM树的配合。对于用户来说,如果先绘制,等CSS解析完成之后再重绘,体验肯定不好。所以浏览器选择再CSS解析完成之后再绘制页面,看得出CSS对于页面的优化也很重要。一些优化CSS的方法如下:
删除不必要的样式:这看起来很明显,但事实上很多人经常会在CSS中加上一些没必要甚至是无效的CSS
比如:
1
2
3
4div {
display: none; //元素会从渲染树中被移除,不会渲染
...其他css属性
}1
2
3
4
5div {
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.