一.浏览器渲染原理
1.浏览器渲染引擎的主要模块以及渲染过程
一个渲染引擎主要包括:HTML解析器,CSS解析器,JS引擎,布局Layout模块,绘图模块等。
HTML解析器:主要用来解析HTML文档,将HTML中的元素组织为DOM树
CSS解析器:为DOM中的各个元素对象计算出样式信息
Javascript引擎:使用js代码可以修改网页的内容,以及更改样式,JavaScript引擎能够解释JavaScript代码
布局模块:DOM创建之后,需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,对元素进行布局
绘图模块:将布局计算后的各个网页节点绘制成图像结果
浏览器的大致渲染过程:
1.首先解析HTML标记,调用HTML解析器解析的对应的token(一个token就是一个标签文本的序列化)并且构建DOM树(一块内存保存着我们解析出来的tokens并建立联系)
2.遇到link标记调用相应解析器处理CSS标记,并且构建出CSS样式树
3.遇见script,调用JavaScript引擎处理script标记、绑定事件、修改DOM树/CSS树等
4.将DOM树与CSS树合并成一个渲染树
5.根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖GPU)
6.最终将每个节点绘制到屏幕上
3. style样式渲染
首先style标签中的样式由HTML解析器进行解析,页面style标签写的内部样式使异步解析的。浏览器加载资源也是异步的。
style标签样式容易出现闪屏现象,这种现象的解释就是当我们html解析器异步解析html结构和style表中的样式时,html的结构的解析先于样式,这时我们的html结构有了但是部分样式却没有出来,这就会导致我们闪屏现象的产生。
因为我们再开发过程中也要尽量的避免使用style标签
4. link样式渲染
link进来的样式,是由CSS解析器进行解析的,并且CSS解析器在解析样式的过程中会阻塞当前的浏览器渲染过程,也就是说它是同步的。
既然它可以阻塞当前的页面渲染,那么它就可以避免闪屏现象,因为我们最多只是等一会加载的过程,我们完全可以给其加一个loading。这样就可以给用户带来相对较好的用户体验,所以我们推荐使用。
5.阻塞渲染
关于CSS的阻塞
只有link引入的css才能够产生阻塞。因为它使用了CSS解析器进行解析。style里的样式使用的事HTML解析器进行解析
style标签中样式:
- 由HTML解析器进行解析
- 不阻塞浏览器的渲染(因此会出现“闪屏现象”,结构先于样式的渲染)
- 不阻塞样式的解析(既然不阻塞渲染,那当然毋庸置疑也就不会阻塞解析了)
link引入的外部css样式:推荐使用的方式
- 由CSS解析器解析
- 阻塞浏览器的渲染(因此可以防止“闪屏”现象的发生)
- 阻塞后面的JS代码执行(JS有操作样式的功能,避免发生冲突所以阻塞,如果不阻塞那么我们不知道最后的样式生效到底是因为css里的操作还是js里的操作)
- 不阻塞DOM的解析(解析完后不代表可以渲染)
优化css的核心概念:尽可能快的提高外部css加载速度
- 使用CDN节点进行外部资源的加速
- 对css进行压缩(利用打包工具,比如webpack;gulp等)
- 减少http请求数,将多个css进行合并
- 优化样式表的代码
关于js的阻塞
- 阻塞后续dom的解析:浏览器不知道后续脚本的内容,如果我们去解析了DOM,但是后面我的js里有对DOM的操作,比如删除了某些DOM,那么浏览器对下面某些DOM的解析就成为了无用功,浏览器无法预估脚本里面具体做了什么。那索性我们就直接阻塞页面的解析
- 阻塞页面的渲染:其实和上面的解释差不多,还是浏览器担心做一下无用功,js可以操作DOM
- 阻塞后续js的执行:上下依赖关系
备注:
- css的解析和hs的解析是互斥的,css解析的时候js停止解析,js解析的时候css停止解析
- 无论css阻塞还是js阻塞,都不会阻塞浏览器引用外部资源(图片、样式、脚本等),因为这样是浏览器加载文档的一种模式,只要是设计网络请求的内容,无论是图片、样式、脚本都会先发送请求去获取资源,至于资源到本地以后浏览器再去协调什么时候使用。
- 浏览器的预解析优化:当下的主流浏览器都有这一功能,就是在执行js代码时,浏览器会再去开一个线程去快速解析文档的其余部分,如果后续的操作有网络请求,则发请求;如果后面的代码没有操作DOM的操作,那就打开阻塞,让浏览器去解析DOM(这时和js阻塞相悖的,但是也是现代浏览器的一种优化方案)
二. 重绘与重排
1.什么是css图层
浏览器再渲染页面时,会将页面分为很多个图层,图层有大有小,每个图层上面有一个或者多个节点。
再渲染DOM时,浏览器所做的实际工作是:
- 获取DOM后将其分隔为多个图层
- 对每个图层的节点计算样式结果
- 为每个节点生成图形和位置(重排,回流)
- 将每个节点绘制填充到图层位图中(重绘)
- 图层作为纹理上传至GPU
- 组合多个图层到页面上最终生成图片
2.图层创建的条件
Chrome浏览器满足以下任意情况就会创建图层:
- 拥有具有3D变换的CSS属性
- 使用加速视频解码的节点
<canvas>
节点- CSS3动画的节点
- 拥有CSS加速属性的元素(Will-change)
3.重绘
重绘是一个元素的外观的改变所触发的浏览器行为,例如改变outline、背景色,以及其他改变元素本身样式的行为。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观,重绘不会带来重新布局,所以重绘不一定重排。
注意:重绘以图层为单位,如果图层中某个元素需要重绘,那么整个图层都需要重绘。所以为了提供性能,我们应该让这些“变化的东西”拥有一个自己的图层。避免引起牵一发而动全身的局面。
4.重排(回流)
渲染对象再创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程被称为布局或者重排。
“重绘”不一定需要“重排”,比如改变某个网页元素的颜色,就只会触发“重绘”,不会触发“重排”,因为布局没有改变,不会重新计算
“重排”一般都会导致“重绘”,比如改变一个网页元素的位置,就会同时触发“重排”和“重绘”,因为布局改变了。
5.常见的触发重排的操作
重排的成本比重绘的成本高很多
一个节点的重排很有可能导致子节点,甚至父节点以及同级节点的重排。
所以,下面这些动作很大可能会是成本比较高的。
- 增删改DOM节点
- 移动DOM位置
- 修改CSS样式
- 缩放窗口位置
- 获取某些没有设置尺寸的DOM的尺寸时
注:display:none会触发重排,visibility:hidden只会触发重绘,因为没有发生位置的变化
6.关于重绘重排的优化方案
如果我们需要提升性能,需要做的就是减少浏览器再运行时所需要做的重复率较高的工作,就是减少我们重绘重排的次数。
具体由以下方案:
- 元素位置移动变换时尽量使用css3的transftransorm来替代top、left等操作。 (使用浏览器调试可以得出发现transftransorm不会重排也不会重绘,底层使用的是图层的组合)
- 使用opacity来替代visiblility。我们使用visibility不会触发重排,但是依然会重绘;我们直接使用opacity即会触发重排也会触发重绘,只有opacity配合图层使用的时候,既不触发重绘也不触发重排。
- 不要使用table布局
- 将多次改变样式属性的操作合并成一次,可以预先将一起的操作放在一个class里再去使用
- 将DOM离线后再去修改,可以再操作DOM时我们使用
display:none
将DOM隐藏,再去疯狂修改,这时浏览器不会进行重排,只有我们显示和隐藏两次重绘重排 - 利用文档碎片documentFragment,也是将发生DOM操作的一部分抽离到碎片中,碎片再内存中操作DOM,性能得到提升
- 不要把获取DOM节点的属性值放在一个循环里当成循环的变量
- 为动画的元素新建图层,提供动画的z-index
- 编写动画时尽量使用requestAnimationFrame
7.requestAnimationFrame与定时器setTimeout的区别
在用法上几乎没有什么区别,就是requestAnimationFrame不需要去设置时间。
我们知道setTimeout在有些时候并不准时,由于Js时单线程执行的,所以会先执行同步代码,将定时器放在定时器模块,等时间完成后再放入事件轮询中等待同步代码和微任务执行完成后再去执行当前的回调函数。所以我们不能精准的调用。
requestAnimationFrame不是由js控制事件间隔的,而是屏幕的刷新频率同步,所以不会受Js单线程的影响。
显示器一般频率为60HZ,所以我们可以计算出没1000ms/60hz=16ms 刷新一次
动画的刷新频率如果大于16时我们就会出现抖动的现象,所以我们需要一个更加平滑的动画
所以我们编写动画时使用requestAnimationFrame。并且它可以浏览器在下次重绘之前调用指定的回调函数更新动画。
我们模拟一个进度条的实现:
let bb = document.getElementById('bb');
let width = 0;
let timmer3 = requestAnimationFrame(function fn() {
if (width > 400) {
cancelAnimationFrame(timmer3)
} else {
width += 5;
bb.style.width = width + 'px'
console.log('执行')
timmer3 = requestAnimationFrame(fn)
}
});
三. CDN
1.CDN的概念
内容分发网络,实际上就是去寻找距离用户最近的服务器,去更加快速、安全的访问资源,来提供高性能、可扩展性及低成本的网络内容传递给用户。
典型的CDN系统由以下三部分组成:
- 分发服务系统
- 负载均衡系统
- 运营管理系统
2.CDN的作用
CDN用于加速用户对Web资源的访问。
再性能方面:
- 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
- 部分资源请求分配给了CDN,减少了服务器的负载
再安全方面:
CDN有助于防御DDoS、MITM等网络攻击
- 针对DDoS:通过监控分析异常流量,限制其请求频率
- 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信
除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势
3.CDN的使用常见
- **使用第三方的CDN服务:**如果想要开源一些项目,可以使用第三方的CDN服务
- **使用CDN进行静态资源的缓存:**将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
四.防抖节流
1.函数防抖
防抖其实就是延迟我们要执行的动作,若在延迟的这段时间内事件出发了,就又从我们触发事件的这个时刻开始延迟。
举一个生活中的小栗子,就是我们的电脑屏幕会隔一段时间息屏,假设我们的电脑设置为一分钟不操作的话会息屏,那么我们在40秒的时候去动了一下鼠标,那计时器会重新从1分钟开始计时。
在我们开发中的应用:搜索时我们定时向服务器发送请求,而不是没输出一个字符就像服务器发送请求,这样做可以减轻服务器的压力,但是经过我的测试一般搜索引擎是不会做防抖操作,而是我们非主打搜索的网站一般在搜索功能上会做防抖优化。
实现:我们假设搜索的场景
let aa = document.getElementById('aa')
let timmer
aa.addEventListener('input', (e) => {
if (timmer) {
clearTimeout(timmer)
}
timmer = setTimeout(() => {
console.log('用户输入了值' + aa.value)
}, 1000);
})
大概思路就是在每次执行事件时清除上一次的定时器,然后重新开一个定时器执行我们的任务。
2.函数节流
函数节流的定义是设定一个特定的时间, 让函数在特定的事件内只执行一次,不会频繁的执行。
同样举个生活中的栗子,我们完fps射击类游戏的时候,一般自动步枪有连发模式和单点模式,单点模式我们点一次调用一次,而连发模式并不是我们按住不放子弹就行连成一条线,而是子弹有事件间隔的射出。
我们模拟一个场景实现一个节流函数:
假设我们需要滚动鼠标滚轮使某个方法有每隔1s执行
let flag = true;
document.body.onscroll = function () {
if (flag) {
console.log('123')
flag=false
setTimeout(() => {
flag = true
}, 1000);
}
}
我们使用标记方式来控制函数的执行,执行完操作后关闭标记,顺便设置响应时间的定时器,定时器任务执行后我们就又可操作了。
五.懒加载
1.懒加载的概念
延迟加载,按需加载,也就是说图片的加载是按照我们网页的位置加载的,一般用在长网页中。如果我们没有使用懒加载,那么当我们一进入网页就要将所有的图片请求下来,这不管是对于服务器还是用户都是一个不好的体验。
所以懒加载就可以解决这个问题。
在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中
特点:
- 减少无用资源的加载:可视区以外的都是无用资源
- 提升了用户体验
- 防止过多的图片资源加载而影响其他资源文件的加载
2.懒加载的实现
使用了html的自定义属性,data-xxx
,图片的加载是由src引起的,当对src赋值时,浏览器就会请求这些资源。所以我们将原本src里面的值存储在data-xxx中,当使用是,再将其值赋给src即可。这样就实现了懒加载
下面我们使用原生JavaScript来实现懒加载:
首先要知道几个属性:
(1)
document.documentElement.clientHeight
是浏览器可视区的高度(2)
document.body.scrollTop || document.documentElement.scrollTop
是浏览器滚动的过的距离(3)
imgs.offsetTop
是元素顶部距离文档顶部的高度(包括滚动条的距离)(4)图片加载条件:
img.offsetTop < window.innerHeight + document.body.scrollTop;
意思就是当前的图片距离顶部的位置如果小于了当前以用的全部空间,那么就说明,到我们图片展示了。
function lazyload(imgs) {
// 获取屏幕的高度
var height=document.documentElement.clientHeight;
// 获取滚轮的位置
var scrollTop=document.documentElement.scrollTop;
// 遍历图片(根据其高度判断其显示状态)
for (let n = 0; n < imgs.length; n++) {
// 某图片离顶部位置<页面高度+滚轮位置(已用空间)
if (imgs[n].offsetTop<height+scrollTop) {
if (!imgs[n].getAttribute('src')) {
imgs[n].setAttribute('src',imgs[n].getAttribute('data-src'));
// n=n+1
}
}
}
}
3.懒加载和预加载的区别
这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
- 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
- **预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。**通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。
预加载的实现主要有以下两种:
- 用CSS实现预加载
- 用JavaScript实现预加载;
六.浏览器缓存机制
1.对缓存的理解
浏览器的缓存主要针对的时前端的静态资源,在发起请求后,拉取到服务器上的资源并且保存在本地内存或者磁盘中。在下一次发送同样的请求时就可以直接在本地拿取资源。如果服务器上的资源已经更新我们就再次去请求资源并且保存在本地。这样做的好处是大大减少了请求的次数,提高了网站的性能。
使用浏览器的缓存主要有以下的优点:
- 加快了客户端加载资源的速度
- 减轻了服务器的压力
- 减少了多次的网络传输
2.浏览器缓存的分类
强缓存
如果缓存资源有效,直接从本地缓存中获取数据,不必发起请求。
强缓存策略可以通过两种方式来设置,分别是http响应头信息中的Expires
属性和Cache-Control
属性
需要注意的是Expires属性是http1.0中的属性,它指定的是强缓存的有效日期,有时会和客户端的时间有偏差。所以http1.1引出了cache-control的属性,这属性是一个有效时期,类似于保质期之类的,可以对客户端缓存做出更加精确的控制。
一般而言的话只设置其中即可,要是两者同时存在的话Cache-Control 的优先级要高于 Expires。
协商缓存
向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存。如果命中,服务器会返回304的状态码通知浏览器从缓存中读取资源
协商缓存也有常见的两种方式来判断是否缓存
服务器返回的Last-Modified和客户端请求时的if-modified-since
服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。
缺点:这两个的属性值时一个是时间值,只能精确到秒,但是如果我们服务器的时间单位是毫秒时就会出现问题。
所以就引出了:Etag和 If-None-Match 属性
这两个属性和上面的工作流程相似,只是属性值不同,这个属性值时一个唯一标识符。不会出现时间混乱的情况。
注:Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高
总结:
强缓存和协商缓存命中后都会拿取本地缓存的资源,区别就是协商缓存会向服务器发送一次请求。它们缓存不命中时都会正常请求。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
3.浏览器的缓存机制的全过程
- 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;
- 第二次访问时,先进行强缓存,于我们的expires的值和我们当前的值对比(如果设置了cache-control则使用cache-contral中的值进行对比),如果命中,则直接在缓存中读取,
- 如果资源已过期,则开始协商缓存,向服务器发送带有if-modified-since或者if-none-Match的请求,判断服务器资源是否更改
- 服务器收到请求后,优先使用etag的值来根据if-none-match的值判断资源是否已修改,如果未修改则命中协商缓存,返回304状态码;如果不一致直接返回请求的资源,状态码为200(如果没有etag的值就是用last-modified的值)