浏览器架构
线程:操作系统能够进行运算调度的最小单位。
进程:操作系统最核心的就是进程,他是操作系统进行资源分配和调度的基本单位。
一个进程就是一个程序的运行实例。启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,把这样的一个运行环境叫进程。
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。
进程和线程之间的关系4个特点。
- 进程中任意线程执行出错,都会导致整个进程崩溃
- 线程之间共享进程中的数据
- 当一个进程关闭后,操作系统会回收进程占用的内存
- 进程之间内容相互隔离(通过IPC机制进行通信)
现代浏览器为多进程架构,打开一个页面,浏览器至少会打开四个进程 -- 浏览器主进程、渲染进程、GPU进程、网络进程。
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU进程:实现3D CSS效果,UI界面绘制。
- 网络进程。主要负责页面的网络资源加载。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
地址栏键入URL后会发生什么?
- 构建请求:浏览器准备发起网络请求,构建请求行等
- 查找缓存:若命中强缓存,直接拦截请求,从本地获取资源
#URL解析
先进行url解析,提取出协议,端口,域名,路径等信息
判断你输入的是一个合法的URL
还是一个待搜索的关键词,并且根据你输入的内容进行对应操作
#DNS查询
获取到了域名对应的目标服务器IP
地址
DNS(Domain Name System)是互联网中用于将域名转换为对应 IP 地址的分布式数据库系统。当客户端需要解析域名时,它会发送 DNS 查询请求到 DNS 服务器,并获取对应的 IP 地址。DNS 查询过程可以分为递归查询和迭代查询两种方式。
### 递归查询
1. **客户端发起请求**:当客户端需要解析域名时,它会发送一个 DNS 查询请求到本地 DNS 服务器。
2. **本地 DNS 服务器查询**:本地 DNS 服务器首先检查自己的缓存中是否有该域名对应的 IP 地址。如果有,它会直接返回给客户端。
3. **迭代查询**:如果本地 DNS 服务器的缓存中没有该域名对应的 IP 地址,它会向根域名服务器发送一个 DNS 查询请求。根域名服务器会告诉本地 DNS 服务器负责该顶级域的权威 DNS 服务器的地址。
4. **查询顶级域 DNS 服务器**:本地 DNS 服务器再向负责该顶级域的权威 DNS 服务器发送一个 DNS 查询请求。顶级域 DNS 服务器会告诉本地 DNS 服务器负责该二级域的权威 DNS 服务器的地址。
5. **查询权威 DNS 服务器**:本地 DNS 服务器继续向负责该二级域的权威 DNS 服务器发送 DNS 查询请求,直到最终获取到域名对应的 IP 地址。本地 DNS 服务器将 IP 地址返回给客户端,并将 IP 地址缓存起来,以便下次查询时使用。
### 迭代查询
1. **客户端发起请求**:客户端发送一个 DNS 查询请求到 DNS 服务器。
2. **DNS 服务器查询**:DNS 服务器首先检查自己的缓存中是否有该域名对应的 IP 地址。如果有,它会直接返回给客户端。
3. **迭代查询**:如果 DNS 服务器的缓存中没有该域名对应的 IP 地址,它会向根域名服务器发送一个 DNS 查询请求。根域名服务器会告诉 DNS 服务器负责该顶级域的权威 DNS 服务器的地址。
4. **查询顶级域 DNS 服务器**:DNS 服务器再向负责该顶级域的权威 DNS 服务器发送一个 DNS 查询请求。顶级域 DNS 服务器会告诉 DNS 服务器负责该二级域的权威 DNS 服务器的地址。
5. **查询权威 DNS 服务器**:DNS 服务器继续向负责该二级域的权威 DNS 服务器发送 DNS 查询请求,直到最终获取到域名对应的 IP 地址。然后,DNS 服务器将 IP 地址返回给客户端。
=======
递归查询和迭代查询的主要区别在于,递归查询是由 DNS 服务器代为向其他 DNS 服务器查询,直到找到域名对应的 IP 地址并返回给客户端;而迭代查询是由 DNS 服务器一步步地指导客户端自行向其他 DNS 服务器查询,直到最终找到域名对应的 IP 地址。
实际上,DNS 解析过程中既有递归查询,也有迭代查询,这两种查询方式是相辅相成的。
1. **递归查询**:在 DNS 解析过程中,客户端向本地 DNS 服务器发起递归查询请求。本地 DNS 服务器会负责向其他 DNS 服务器发送递归查询请求,直到找到域名对应的 IP 地址,并将结果返回给客户端。递归查询的特点是 DNS 服务器会负责完成整个查询过程,直到找到结果并返回给客户端。
2. **迭代查询**:在 DNS 解析过程中,当本地 DNS 服务器无法直接返回结果时,它会向其他 DNS 服务器发送迭代查询请求,获取更多的信息。这些迭代查询请求会依次向根域名服务器、顶级域 DNS 服务器和权威 DNS 服务器发送,直到最终找到结果。迭代查询的特点是 DNS 服务器向客户端提供其他 DNS 服务器的地址,然后客户端会继续向这些 DNS 服务器发送查询请求,直到找到结果。
因此,DNS 解析过程中通常会同时使用递归查询和迭代查询,以确保客户端能够快速准确地获取到域名对应的 IP 地址。
#TCP连接
在确定目标服务器服务器的IP
地址后,则经历三次握手建立TCP
连接
#发送 http 请求
当建立tcp
连接之后,就可以在这基础上进行通信,浏览器发送 http
请求到目标服务器
请求的内容包括:
- 请求行
- 请求头
- 请求主体
#响应请求
当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个HTTP
响应消息,包括:
- 状态行
- 响应头
- 响应正文
在服务器响应之后,由于现在http
默认开始长连接keep-alive
,当页面关闭之后,tcp
链接则会经过四次挥手完成断开
#页面渲染
当浏览器接收到服务器响应的资源后,首先会对资源进行解析:
- 查看响应头的信息,根据不同的指示做对应处理,比如重定向(301,302),存储cookie(如果响应头中包含 Set-Cookie 字段,表示服务器希望浏览器存储 cookie,浏览器会根据这些字段存储 cookie 信息。),解压gzip(Content-Encoding :gzip),缓存资源等等
- 查看响应头的 Content-Type的值,根据不同的资源类型采用不同的解析方式
(Content-Type 是响应头中的一个字段,它指示了响应内容的类型。浏览器会根据 Content-Type 的值来确定响应的内容类型,然后采用不同的解析方式对响应内容进行处理。例如:
- 如果 Content-Type 表示响应内容是 HTML 文档(如
text/html
),浏览器会将其解析为 DOM 结构,并进行页面渲染。 - 如果 Content-Type 表示响应内容是 JavaScript 脚本(如
application/javascript
),浏览器会执行这段 JavaScript 代码。 - 如果 Content-Type 表示响应内容是 CSS 样式表(如
text/css
),浏览器会解析这段 CSS 代码,并应用于页面渲染。 - 如果 Content-Type 表示响应内容是图片、音频或视频等媒体资源(如
image/jpeg
、audio/mp3
、video/mp4
等),浏览器会直接将其展示给用户或者交给相应的插件进行处理。)
渲染流程:HTML、CSS和JavaScript,是如何变成页面的?
关于页面的渲染过程如下:dom css render 布局绘制显示
- 解析HTML,构建 DOM 树
- 解析 CSS ,生成 CSS 规则树 样式计算
- 合并 DOM 树和 CSS 规则,生成 render 树,(在这个阶段,浏览器会根据 DOM 树中的每个可见节点和样式计算得到的具体样式来构建渲染树,忽略掉不可见的元素或不影响渲染的节点(比如
<script>
、<meta>
等)。这个渲染树将用于后续的布局和绘制。) - 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
- 绘制 render 树( paint ),绘制页面像素信息
- 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成,显示在屏幕上
-
构建DOM树:浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树
-
样式计算:
- 把CSS转换为浏览器能够理解的结构 -- styleSheets
- 转换样式表中的属性值,使其标准化,比如将颜色‘red’转化成渲染引擎可以理解的rgb值
- 浏览器会将DOM树和CSS规则树合并,计算每个元素的具体样式信息,即进行样式计算。这个过程中,浏览器会计算出每个DOM节点的最终样式信息,包括继承的样式和自身的样式。
-
布局阶段:有了DOM树和样式,还不足以显示页面,接下来就需要计算出DOM树中可见元素的几何位置,这个计算过程叫做布局。
- 创建布局树:构建一颗元素布局树,比如去掉head等没有实际元素的标签,去掉display为none的元素等
-
分层:页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。所以浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面
-
图层绘制:将布局树中的元素转换成屏幕上的像素信息。
-
栅格化操作:通常一个页面可能很大,但是用户只能看到其中的一部分,这个部分叫做视口。在有些情况下,有的图层可以很大,比如有的页面要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块,这些图块的大小通常是256x256或者512x512。然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。
-
合成和显示:所有图块都栅格化完成后,合成线程将指令提交给浏览器进程,浏览器进程根据指令将页面内容绘制到内存中,最后从内存中取出图像显示到屏幕上。
从发起URL请求开始,到首次显示页面的内容,在视觉上经历三个阶段。可能会有阻塞的地方,怎么解决
页面渲染过程中可能出现阻塞的主要环节包括:
- 等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。
- 提交数据之后渲染进程会创建一个空白页面,这段时间称为解析白屏,并等待CSS文件和JavaScript文件的加载完成,生成CSS规则树和dom树,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
- 第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
影响第一个阶段的因素主要是网络或者是服务器处理。
第二个阶段主要问题是白屏时间,如果白屏时间过久,就会影响到用户体验。为了缩短白屏时间,需要挨个分析这个阶段的主要任务,包括了解析HTML、下载CSS、下载Js、生成CSSOM、执行JavaScript、生成布局树、绘制页面一系列操作。通常情况下的瓶颈主要体现在下载CSS文件、下载JavaScript文件和执行JavaScript。
1. HTML解析:浏览器解析HTML文档构建DOM树时,如果遇到`<script>`标签,会立即暂停解析,等待脚本执行完毕。这是因为脚本可能会修改DOM,所以浏览器需要等待脚本执行完成后再继续解析。
2. CSS解析:CSSOM(CSS对象模型)的构建也可能阻塞渲染,因为浏览器需要确保页面的样式在脚本执行之前就已经确定。
3. JavaScript执行:JavaScript的执行会阻塞DOM的构建,因为脚本可能会操作DOM元素。
所以要想缩短白屏时长,可以有以下策略:
- 将关键CSS直接内联到HTML中(style标签),可以加快首次渲染的速度(行内>内部>外部)。对于非关键CSS,可以通过媒体查询或JavaScript异步加载。这样只有在特定的场景下才会加载特定的CSS文件。
-
<!-- 异步加载非关键CSS --> <script> var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'non-critical.css'; document.head.appendChild(link); </script> </head>
- 异步加载脚本:使用
async
或defer
属性将JavaScript脚本异步加载,可以使得脚本不会阻塞页面的渲染,从而减少白屏时间。 async:异步加载脚本,脚本加载完成后,会立即执行,与在页面中出现的顺序无关; defer:浏览器立即加载,但是延迟执行,需要在DOMContentLoaded事件之前执行,与出现顺序有关,dom加载完毕后按出现顺序依次执行。 -
使用CDN加速:将页面资源分发到全球各地的CDN(内容分发网络),可以加速资源的加载速度,减少白屏时间。CDN可以使用户从距离较近的服务器获取资源,从而提高访问速度。
- 使用预加载和预渲染技术:可以使用预加载(preload)和预渲染(prerender)等技术,提前加载页面所需的资源或者预渲染页面内容,以减少白屏时间。预加载可以在页面加载过程中提前加载关键资源,而预渲染可以在后台加载整个页面内容,使得用户访问页面时可以立即展示完整内容。
- HTTP/2通过引入多路复用、头部压缩、服务器推送等技术,优化了网络资源的利用效率,减少了网络连接的延迟,提高了页面加载速度
- 代码分割和懒加载:可以使用代码分割将代码分成多个小块,然后按需加载。对于不影响首屏渲染的组件或功能,可以使用懒加载技术延迟加载。Webpack提供了一种称为“动态导入”的语法,可以实现代码分割。
// 使用动态导入语法,将代码按需分割成多个小块
import('./module').then(module => {
// 在需要的时候加载模块
module.doSomething();
});
Webpack会将动态导入的代码块分割成单独的文件,并在需要的时候按需加载。
在React中,可以使用React.lazy和Suspense组件来实现懒加载。
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
当 MyComponent 被渲染时,它会先显示 fallback 属性指定的加载中提示,
然后在 LazyComponent 加载完成后显示真正的内容。
- 客户端缓存:浏览器可以通过缓存响应内容来减少对服务器的请求。通过设置合适的缓存控制头(如
Cache-Control
和Expires
),可以告诉浏览器何时可以使用缓存以及缓存的有效期。对于那些不经常变化的静态资源,可以使用强缓存来尽快地加载页面内容,减少白屏时间;而对于那些可能会频繁更新的动态内容,可以使用协商缓存来确保客户端可以获取到最新的资源。在Express中,你可以在路由处理函数中设置缓存控制头,通过res.setHeader
方法实现。
代码优化
比如下面的代码,实现了一个斐波那契数列,也就是说,在实现的这个数列中,每一个数的值是前面两个数的值之和。可以使用简单的递归算法实现斐波那契数列后,在页面显示计算结果。
function fib(n){ if(n<=1) return 1 return fib(n-1)+fib(n-2) } let count = ref(fib(38))
使用performance:
打开调试窗口中的 Performance 面板,使用录制功能后,便可得到下面的火焰图。通过这个火焰图,可以清晰地定位出这个项目中,整体而言耗时最长的 fib 函数,并且能看到这个函数被递归执行了无数次。到这里,不难意识到这段代码有性能问题。不过,定位到问题出现的地方之后,代码性能的优化就变得方向明确了。
用户体验优化
1.比如用户加载大量图片的同时,如果本身图片清晰度较高,那直接加载的话,页面会有很多图一直是白框。所以可以预先解析出图片的一个模糊版本,加载图片的时候,先加载这个模糊的图作为占位符,然后再去加载清晰的版本。虽然额外加载了图片文件,但是用户在体验上得到了提升。
2.类似的场景还有很多,比如用户上传文件的时候,如果文件过大,那么上传可能就会很耗时。而且一旦上传的过程中发生了网络中断,那上传就前功尽弃了。为了提高用户的体验,可以选择断点续传,也就是把文件切分成小块后,挨个上传。这样即使中间上传中断,但下次再上传时,只上传缺失的那些部分就可以了。可以看到,断点上传虽然在性能上,会造成网络请求变多的问题,但也极大地提高了用户上传的体验。
3.还有很多组件库也会提供骨架图的组件,能够在页面还没有解析完成之前,先渲染一个页面的骨架和 loading 的状态,这样用户在页面加载的等待期就不至于一直白屏
性能监测报告
解释一下 FCP、TTI 和 LCP 这几个关键指标的含义。
首先是 First Contentful Paint,通常简写为 FCP,它表示的是页面上呈现第一个 DOM 元素的时间。在此之前,页面都是白屏的状态;
然后是 Time to interactive,通常简写为 TTI,也就是页面可以开始交互的时间;
还有和用户体验相关的 Largest Contentful Paint,通常简写为 LCP,这是页面视口上最大的图片或者文本块渲染的时间,在这个时间,用户能看到渲染基本完成后的首页,这也是用户体验里非常重要的一个指标。
可以通过代码中的 performance 对象去动态获取性能指标数据,并且统一发送给后端,实现网页性能的监控。性能监控也是大型项目必备的监控系统之一,可以获取到用户电脑上项目运行的状态。下图展示了 performance 中所有的性能指标,可以通过这些指标计算出需要统计的性能结果。
const timing = window.performance && window.performance.timing
const navigation = window.performance && window.performance.navigation
// DNS 解析:
const dns = timing.domainLookupEnd - timing.domainLookupStart
// 总体网络交互耗时:
const network = timing.responseEnd - timing.navigationStart
// 渲染处理:
const processing = (timing.domComplete || timing.domLoading) - timing.domLoading
// 可交互:
const active = timing.domInteractive - timing.navigationStart
在上面的代码中,通过 Performance API 获取了 DNS 解析、网络、渲染和可交互的时间消耗。有了这些指标后,就可以随时对用户端的性能进行检测,做到提前发现问题,提高项目的稳定性。