1 在浏览器上输入一个网址到页面呈现的过程?
1.1 域名解析,DNS查询,找到域名对应的IP地址
百度: 14.215.177.39
浏览器缓存和本地的 hosts 文件;
本地 DNS 解析器缓存;
DNS 服务器;
根 DNS 服务器,获取到顶级域名(.com)对应的IP地址
顶级域(baidu.com) DNS 服务器;
权威 DNS 服务器(www.baidu.com);
1.2 TCP三次握手,建立TCP连接
TCP 和 UDP?
TCP 和 UDP 都是负责提供端到端通信的传输层协议。TCP 是面向连接的协议,在源和目标之间建立连接;而 UDP 是无连接协议,既不建立连接,也不检查目标计算机是否已准备好接收,只是将数据发送到目标计算机
为什么要进行三次握手?
防止重复连接:在网络差的情况下,客户端可能会发送两次连接,如果是三次握手可以过滤掉旧的连接
同步初始化序列化:初始化发送数据的序列号,保证双方都进入数据可发送和接受的状态
当浏览器获取到服务器的 IP 地址后,浏览器会用一个随机的端口(1024 < 端口 < 65535)向服务器 80(HTTP 默认约定 80 端口,HTTPS 为 443 端口) 端口发起 TCP 连接请求。这个连接请求到达服务端后,通过 TCP 三次握手,建立 TCP 的连接:
1.客户端发送 SYN 包(seq = j)到服务器,并进入 SYN_SEND 状态,等待服务器确认。
2.服务器收到 SYN 包,必须确认客户的 SYN(ACK = j + 1),同时自己也发送一个 SYN 包(seq = k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态。
3.客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK = k + 1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。
1.3 客户端发送http请求
HTTPS和HTTP的区别:
-
https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
-
http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
-
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
-
http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
建立连接后就可以通过 HTTP 进行数据传输。如果使用 HTTPS,会在 TCP 与 HTTP 之间多添加一层协议做加密及认证的服务。HTTPS 使用 SSL和 TLS协议,保障了信息的安全。
SSL/TSL握手:
1.客户端发起https请求,请求中携带数据:SSL/TSL版本号、加密套件即客户端支持的加密算法、随机数A、Client Hello字符串
2.服务器收到请求,向客户端发出响应,携带数据:确认SSL/TSL版本号、确认的加密算法列表、随机数B
3.服务器再向客户端发送数字证书,服务器会把自己的公钥注册到CA(第三方证书机构),然后CA拿自己的私钥对服务器的公钥进行处理并颁发数字证书,将公钥发送给客户端,而与之对应的私钥保留在服务端不公开,最后发送Hello Done表示发送完毕
4.客户端收到数据,检验数字证书的合法性,检验通过后向服务器发送:随机数C(通过公钥加密)
5.服务端收到数据,使用公钥配对的私钥对随机数C解密, 然后服务端通过随机数A、B、C计算出会话密钥,同时客户端也计算出了会话密钥,使用会话密钥对要传输的HTTP数据进行对称加密将密文返回客户端
6.客户端会话密钥对称解密密文,得到HTTP数据明文
7.后续HTTPS请求使用之前交换好的会话密钥进行对称加解密。
非对称加密:和对称加密有所区别的就是非对称加密有两个密钥:一个是公钥,一个是私钥。有两种实现方法:
1.用公钥加密,用私钥解密
2.用私钥加密,用公钥解密
对称加密:加密者和解密者使用的是同一个密钥进行加密或解密。
优化:使用HTTP/2、HTTP/3
1.4 服务端响应请求
当浏览器到 web 服务器的连接建立后,浏览器会发送一个初始的 HTTP GET 请求,请求目标通常是一个 HTML 文件。服务器收到请求后,将发回一个 HTTP 响应报文,内容包括相关响应头和 HTML 正文
状态码:
-
1xx:指示信息——表示请求已接收,继续处理
-
2xx:成功——表示请求已被成功接收、理解、接受
-
3xx:重定向——要完成请求必须进行更进一步的操作
-
4xx:客户端错误——请求有语法错误或请求无法实现
-
5xx:服务器端错误——服务器未能实现合法的请求
优化:使用http缓存( 强缓存和协商缓存)
1.5 客户端渲染
1.5.1 解析HTML,生成DOM树
当浏览器通过接收到页面的HTML数据时,它会立即设置解析器将HTML转换为文档对象模型(DOM)生成DOM树
生成DOM树之后,根节点是document,可以通过根节点去访问子孙节点。比如我们想要访问span标签的时候只需要document.body.children[0].firstElementChild就可以了。如果没有生成DOM树,那获取起来就超级复杂了
1.5.2 获取外部资源
当解析器遇到外部资源(如CSS或JavaScript文件)时,解析器将提取这些文件。
加载CSS文件时****不会影响dom的解析,但会阻止dom的渲染
加载JS文件时会阻止****dom的解析, 不过可以将两个属性添加到脚本标签中以减轻这种情况:defer 和async。两者都允许解析器在后台加载JavaScript 文件的同时继续运行,但是它们的执行方式不同
defer 属性会将 Javascript 脚本延迟执行,但是解析过程中遇到 script 标签仍然会进行下载,defer 脚本会在 dom 解析完成后,DOMContentLoaded事件调用前执行。如果多个文件具有defer属性,则将按照页面放置的顺序依次执行。
<script type="text/javascript" src="script.js" defer>
async 属性会将 Javascript 脚本异步执行,解析过程中遇到 script 标签会进行下载,且下载完成后立即进行异步执行,由于是异步执行,所以当有多个异步的 js 脚本时无法控制先后执行的顺序,
<script type="text/javascript" src="script.js" async>
优化:css请求放在头部,js请求放在尾部
1.5.3 解析 CSS 并构建CSSOM
与HTML文件和DOM相似,加载CSS文件时,必须将它们解析并转换为树-这次是CSSOM。它描述了页面上的所有CSS选择器,它们的层次结构和属性。他有两个步骤:
1、转换样式表的属性值,使其标准化: 比如将颜色值转换成rgb格式
2、 计算出DOM树中每个节点的具体样式: 计算DOM节点的具体样式,需要考虑CSS的继承、样式层叠规则。
在上图中,灰色的就是继承的属性,而黑色则是节点新增属性,包括覆盖掉继承属性的。
1.5.4 执行 JavaScript
解析 JS 是一个昂贵的过程,比其他类型的资源更昂贵,因此优化它对于获得良好的性能是非常重要
1.5.5 合并 DOM树 和 CSSOM树 生成渲染树(render tree)
渲染树是DOM和CSSOM的组合,表示将要渲染到页面上的所有内容。这并不一定意味着渲染树中的所有节点都将在视觉上呈现,例如,将包含opacity: 0或visibility: hidden的样式的节点,并仍然可以被屏幕阅读器等读取,而display: none不包括任何内容。此外,诸如之类的不包含任何视觉信息的标签将始终被忽略
1.5.6 渲染树布局
布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的准确大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。
1.5.7 渲染树绘制
在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
分层
因为页面中有很多复杂的效果,像是3D变换,页面滚动等,为了更方便的实现这些效果,渲染引擎会为特定的节点(定位,裁剪)生成专用的图层,并生成一颗对应的图层树,最后再合成图层。
查看分层:更多工具-图层
创建新图层的场景:定位; 出现要裁剪的时候,渲染引擎回为文字部分当都创建一个图层。滚动条也会是一个图层
绘制
分层结束后,我们会得到图层树,然后渲染引擎就会对图层树上的每个图层进行绘制。而绘制的过程就是模仿画画,会把涂层的绘制拆分成很多个绘画指令。我们想要绘制只需要依次执行一个绘制列表的每一条指令即可,比如,画一个矩形,画一个边框等
光栅化
上一步(绘制)中,我们看到了绘制指令列表。但是实际的绘制操作并不是主线程来完成的,而是合成线程来完成的。
当图层的绘制指令列表准备好之后,主线程会把该列表提交(commit)给合成线程。然后合成线程开始工作:
●合成线程将图层划分为图块(tile)
●图块栅格化:将图块转换为位图(会优先将视口附近的图块先转换为位图)
通常一个页面会很大(长),但是用户只能看到其中一部分,而这一部分叫做视口(viewport)。有一些图层也会很大,但是用户只能通过视口看到一部分,所以就没必要将整个图层都绘制出来。这就是将图层划分成图块的原因
渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。而且栅格化过程中会使用GPU来加速生成位图,使用GPU生成位图的过程叫做快速栅格化,生成的位图会保存在GPU内存中
合成
当所有的图块都被光栅化后,合成线程就会生成一个绘制图块的命令(DrawQuad),然后将该命令提交给浏览器进程。浏览器进程中的组件viz会根据该命令,将页面内容绘制到内存中,最后将页面内容从内存中拿出来,显示在屏幕上
合成操作是在合成线程上完成的,也就是说,执行合成操作时,是不会影响到主线程的
reflow与repaint
回流(重排):发生几何变化,需要重新根据CSSOM和DOM来计算布局树,然后完整执行渲染流水线,包括分层、绘制、合成(光栅化)。
常见的会引起回流的样式:font-size、font-family、width、height、padding、margin、left、top、border、offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
重绘:如果修改元素的背景颜色,不会触发布局、分层阶段,直接进入绘制阶段,然后执行之后的子阶段,这个过程就叫重绘。
常见的会引起重绘的样式:color、backgound、
优化:减少重排
1.6 TCP四次挥手,断开连接
现在的页面为了优化请求的耗时,默认都会开启持久连接(keep-alive),那么一个 TCP 连接确切关闭的时机,是这个 tab 标签页关闭的时候。这个关闭的过程就是四次挥手。关闭是一个全双工的过程,发包的顺序是不一定的。一般来说是客户端主动发起的关闭,过程如下所示:
1.客户端发送一个 FIN,用来关闭服务端到客户端的数据传送,告诉服务端:我已经不会再给你发数据了(在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,客户端依然会重发这些数据),但此时客户端还可以接受数据。
2.服务端收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号)。
3.服务端发送一个 FIN,用来关闭客户端到服务端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发数据了。
4.客户端收到 FIN 后,发送一个 ACK 给被动服务端,确认序号为收到序号+1,至此,完成四次挥手。
总结:上面可以分为两个部分,一是网络层面,二是页面渲染
2 网络请求优化
减少网络资源请求
2.1 从设计实现层面简化页面,保持页面简洁、减少资源的请求
首页一般不会放太多请求,将页面内容尽快地展示给用户,减少页面白屏时间
2.2 使用 HTTP/2、HTTP/3
HTTP/2 的主要改进就在于提高加载资源的速度
它使用了头部压缩,通过特有的 HPACK 算法,让请求头做了极致的压缩。
此外使用了多路复用,让请求产生流的特性,可以同时发送多个请求,充分利用带宽;
2.3 设置http缓存
合理的设置http缓存,利用 HTTP 缓存策略,通过强缓存和协商缓存的配合,让一些资源能够不必再从服务端获取,而是直接复用本地缓存好的资源。
强缓存
强制缓存在缓存数据未失效的情况下,即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期,那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,HTTP的状态码为200。
这种方式页面的加载速度时最快的,性能也是很好的,但是如果在这期间,服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。 Cache-Control优先级高于Expires
协商缓存
当浏览器第一次向服务器发送请求时,会在响应头返回协商缓存的头属性:ETag和Last-Modified,其中ETag返回的是一个hash值,资源标识。Last-Modified返回的是GMT格式的时间,标识该资源的最新修改时间;然后浏览器发送第二次请求的时候,会在请求头中带上If-None-Match(对应ETag)和If-Modified-Since(对应Last-Modified);服务器在接收到这两个参数后会做比较,会优先验证ETag,一致的情况下,才会继续比对Last-Modified;如果返回的是304状态码,则说明请求的资源没有修改,浏览器可以直接在缓存中读取数据,否则,服务器直接返回数据。
// nginx.conf 在nginx配置
etag on; //开启etag验证
expires 7d; //设置缓存过期时间为7天
注意:配置缓存时一定要切记,浏览器在处理用户请求时,如果命中强缓存,浏览器会直接拉取本地缓存,不会与服务器发生任何通信,即在服务器端更新了文件,并不会被浏览器得知,就无法替换失效的缓存。所以我们在构建阶段,需要为我们的静态资源添加md5 hash后缀,避免资源更新而引起的前后端文件无法同步的问题。
2.4 图片资源加载
2.4.1 base64 内联
一些比较小的资源,比如一个不大的图标图片,可以考虑转换成 base64 格式,内嵌到 HTML 中。这样做的目的是减少 HTTP 请求数量
// 将小于 8192 字节的图片转换为 base64。
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
2.4.2 使用WebP
WebP格式,是谷歌公司开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间。Facebook、Ebay等知名网站已经开始测试并使用WebP格式。
webpack项目中使用:
// 1、安装插件 imagemin-webp-webpack-plugin
// 2、config中使用插件
...
new ImageminWebpWebpackPlugin({
config: [
{
test: /\.(jpe?g|png)/,
options: {
quality: 50, //压缩比例
},
},
],
overrideExtension: true,
detailedLogs: false,
silent: false,
strict: true,
}),
...
// 3、定义组件
// WebpImage.tsx
import React from "react";
const WebpImage = (props) => {
const { src } = props;
const webpSrc = React.useMemo(() => {
const nameChunks = src.split(".");
nameChunks.pop();
nameChunks.push("webp");
return nameChunks.join(".");
}, [src]);
return (
<picture>
<source srcSet={webpSrc} type="image/webp" />
<img {...props} />
</picture>
);
};
export default WebpImage;
// 4、使用
import Imgsrc from "@/assets/images/26.jpg";
...
<WebpImage src={Imgsrc} style={{ width: "600px" }} />
...
2.4.3 雪碧图
英文原名为 CSS Sprites,指将多个小图片(比如图标)整合到一张大图片上,下载完后通过 CSS 的 background-position 属性框选需要用到的小图片上。
作用是减少 HTTP 请求数量,以及提前加载好一些图片资源,比如按钮图片的 hover 效果
2.4.4 使用字体图标 iconfont 代替图片图标
不论是压缩后的图片,还是雪碧图,终归还是图片,只要是图片,就还是会占用大量网络传输资源。但是字体图标的出现,却让前端开发者看到了另外一个神奇的世界
在项目中使用:
登陆图标库,将需要的图标加入购物车
创建项目
下载代码
将相关文件放在项目中,并在index.css中引入
组件中使用
2.5 开启 gzip
Gzip是一种文件级别的数据压缩算法,用来减少文件大小,节省带宽从而提高网站的访问速度。它可以有效减少网络传输时间,这在大多数网站上可以大大提升用户体验,例如网站会更快地加载。Gzip是一种很好的优化技术。
浏览器请求url,并在request header中设置属性accept-encoding:gzip。表明浏览器支持gzip。
服务器收到浏览器发送的请求之后,判断浏览器是否支持gzip,如果支持gzip,则向浏览器传送压缩过的内容,不支持则向浏览器发送未经压缩的内容。一般情况下,浏览器和服务器都支持gzip,response headers返回包含content-encoding:gzip。
浏览器接收到服务器的响应之后判断内容是否被压缩,如果被压缩则解压缩显示页面内容
注意:不要对图片文件进行Gzip压缩,一般建议打包文件大于10K开启
前端配置:
// 前端
const CompressionWebpackPlugin = require("compression-webpack-plugin");
...
new CompressionWebpackPlugin({
filename: "[path].gz[query]", // 目标资源名称。[file] 会被替换成原资源。[path] 会被替换成原资源路径,[query] 替换成原查询字符串
algorithm: "gzip", // 算法
test: new RegExp("\\.(js|css|less)$"), // 压缩 js 与 css
threshold: 10240, // 只处理比这个值大的资源。按字节计算
minRatio: 0.8, // 只有压缩率比这个值小的资源才会被处理
}),
...
服务器端配置
// nginx配置 默认不开启
http {
# 开启 gzip 压缩
gzip on;
# 检查是否存在请求静态文件的gz结尾的文件,如果有则直接返回该gz文件内容,不存在则先压缩再返回
gzip_static on;
# 使用 gzip 压缩的文件类型
# 此外,text/html 是自带的,不用写上
gzip_types text/plain text/css application/javascript application/json text/xml application/xml application/xml+rss;
# 小于 256 字节的不压缩
# 这是因为压缩是需要时间的,太小的话压缩收益不大
gzip_min_length 256;
# 开启静态压缩
# 压缩的资源会被缓存下来,下次请求时就直接使用缓存
gzip_static on;
}
2.6 静态资源使用 CDN
内容分发网络,Content Delivery Network或Content Ddistribute Network,简称CDN,是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。
CDN加速的本质是缓存加速。将服务器上存储的静态内容缓存在CDN节点上,当访问这些静态内容时,无需访问服务器源站,就近访问CDN节点即可获取相同内容,从而达到加速的效果,同时减轻服务器源站的压力。
CDN应用广泛,解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度和成功率。
由于访问动态内容时,每次都需要访问服务器,由服务器动态生成实时的数据并返回。因此CDN的缓存加速不适用于加速动态内容,CDN无法缓存实时变化的动态内容。对于动态内容请求,CDN节点只能转发回服务器源站,没有加速效果
3 页面渲染优化
3.1 懒加载
一些暂时不用到的资源先不急着加载,在用到的时候再加载,减少资源加载
懒加载有很多种,有图片懒加载,滚动到图片位置,才开始加载图片(知乎使用了这个,另外 Nextjs 框架的 Image 组件也支持懒加载)。
还有 JS 模块的懒加载,像 ES6 的动态 import,这个在 Webpack 有支持。
还有组件的懒加载:{visible && }
3.2 长列表优化
长列表指的是列表项很多的列表,达到成千上万的规模。
如果要一次性将它们渲染出来,在渲染阶段容易遇到瓶颈,导致页面卡顿。常见的解决方案有两种:
1.使用时间分片:不一次性加载所有列表项,间隔一段时间渲染一批,直至全部渲染完;
2.使用虚拟列表:只渲染可视区域内的列表项
3.3 Web Worker
它的作用就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行结束后把结果返回给主线程。
注意事项:
1、非同源限制,主线程代码与Worker线程代码必须同源才能一起正常工作
2、Worker线程任务需要等待主线程任务结束才能进行
3、可以主动关闭Worker线程。如果是多页应用的话,离开了Worker页面,Worker 也会停止工作
4、脚本限制,worker线程不能执行alert、confirm,但可以使用 XMLHttpRequest 对象发出ajax请求
5、不能操作DOM
3.4 减少重排和重绘
-
尽可能在低层级的DOM节点上,而不是像上述全局范围的示例代码一样,如果你要改变p的样式,class就不要加在div上,通过父元素去影响子元素不好【减少重排范围】
-
CSS属性读写分离:浏览器每次对元素样式进行读操作时,都必须进行一次重新渲染(重排 + 重绘),所以我们在使用JS对元素样式进行读写操作时,最好将两者分离开,先读后写,避免出现两者交叉使用的情况。如不用JS去操作元素样式
// 强制刷新 触发四次重排+重绘
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';
// 缓存布局信息 相当于读写分离 触发一次重排+重绘
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';
- 不要直接去修改样式而是通过切换class或者style.csstext属性去批量操作元素样式
// 当top和left的值是动态计算而成时...
// bad
el.style.left = left + "px";
el.style.top = top + "px";
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
// better
el.className += " className";
-
将 DOM 离线
-
使用 display:none,一旦我们给元素设置 display:none 时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过 display属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。另外,visibility : hidden 的元素只对重绘有影响,不影响重排。
-
通过 documentFragment 创建一个 dom 碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排。
-
复制节点,在副本上工作,然后替换它!
-
-
图片在渲染前指定大小:因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流
-
实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局
-
动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多
4、Lighthouse
使用Lighthouse工具,查看可有优化项