浏览器架构
浏览器架构演进
- 单进程架构:所有模块运行在同一个进程里,包含网络、插件、JavaScript运行环境等
- 多进程架构(现代浏览器的常用架构):主进程、网络进程、渲染进程、GPU进程、插件进程
- 面向服务架构:算是多进程架构的升级版。将原来的UI、数据库、文件、设备、网络等,作为一个独立的基础网络服务
后两者运行在独立沙盒当中,进程相互隔离,安全性比较高,且充分利用系统资源(独立渲染)流畅度高
任务管理器
查看路径:【左上角三个点】→【更多工具】→【任务管理器】
多进程分工
进程名称 | 进程描述 |
---|---|
浏览器(主进程) | 主要负责页面展示逻辑,用户交互,子进程管理;包括地址栏、书签、前进、后退、收藏夹等 |
GPU进程 | 负责UI绘制,包含整个浏览器全部UI |
网络进程 | 网络服务进程,负责网络资源加载 |
标签页(渲染进程) | 控制tab内的所有内容,将HTML、CSS和JavaScript转换为用户可交互的网页 |
插件进程 | 控制网站运行的插件,比如flash、ModHeader等 |
其他进程 | 如上图所示:适用程序Storage/Network/Audio Service等 |
渲染进程
多线程架构
内部是多线程实现,主要负责页面渲染,脚本执行,事件处理,网络请求等
-
解释执行JS
- JS引擎:JS引擎负责解析和执行JavaScript代码。
- 渲染引擎:渲染引擎的主要任务是解析HTML和CSS,构建DOM树和渲染树,然后将渲染树转换为屏幕上的像素。
-
XML解析生成渲染树,显示在屏幕
- 渲染引擎:渲染引擎负责解析HTML和XML文档,构建DOM树。
DOM树是由DOM元素及其属性组成的树状结构。渲染树构建完成后,渲染引擎会根据渲染树来绘制页面。
-
桥接方式通信
- JS引擎:JS引擎提供了DOM API,使得JavaScript代码可以访问和操作DOM树。
渲染进程 - 多线程工作流程
- 网络线程负责加载网页资源
- 网络线程的主要任务是从服务器加载网页资源,如HTML、CSS、JavaScript、图片等。加载过程中,网络线程会将HTML和CSS交给渲染线程进行解析,并将JavaScript交给JS引擎进行解析和执行
- JS引擎解析JS脚本并且执行
- JS引擎将接收到的JavaScript代码解析成抽象语法树(AST),然后通过JIT编译器将AST编译成机器代码,最后执行机器代码。在这个过程中,JS引擎还会进行优化,提高代码的执行效率
- JS解析引擎空闲时,渲染线程立即工作
- 当JS引擎空闲时(即没有JavaScript代码需要执行),渲染线程开始工作。渲染线程负责解析HTML和CSS,构建DOM树和渲染树,计算布局信息,并将渲染树转换为屏幕上的像素。如果在此过程中遇到JavaScript代码(如
<script>
标签或事件处理程序),渲染线程会暂停,将控制权交给JS引擎
- 当JS引擎空闲时(即没有JavaScript代码需要执行),渲染线程开始工作。渲染线程负责解析HTML和CSS,构建DOM树和渲染树,计算布局信息,并将渲染树转换为屏幕上的像素。如果在此过程中遇到JavaScript代码(如
- 用户交互、定时器操作等产生回调函数放入任务队列中
- 当用户与网页进行交互(如点击、滚动等),或者有定时器触发(如
setTimeout
或setInterval
)时,相应的回调函数会被放入任务队列中。任务队列是一个先进先出(FIFO)的数据结构,用于存储待处理的任务
- 当用户与网页进行交互(如点击、滚动等),或者有定时器触发(如
- 事件线程进行事件循环,将队列里的任务取出交给JS引擎执行
- 事件线程负责管理事件循环。事件循环的主要任务是检查任务队列,将队列中的任务按顺序取出,并交给JS引擎执行。JS引擎执行任务时,可能会对DOM树进行操作,从而触发渲染线程的更新。事件循环会持续进行,直到任务队列为空,或者浏览器关闭。
一道笔试题
问题
以下代码会输出什么?为什么?
const now = Date.now();
// 定时器1:10ms后输出"time10"和定时器1的执行延时
setTimeout(() => {
console.log('time10', Date.now() - now); // 输出1:?
}, 10);
// 定时器2:30ms后输出"time30"和定时器2的执行延时
setTimeout(() => {
console.log('time30', Date.now() - now); // 输出2:?
}, 30);
// 一直执行循环,直到距离当前时间戳至少过去20ms
while (true) {
if (Date.now() - now >= 20) {
break;
}
}
console.log(Date.now() - now);
解答
在我的Chrome上结果如下
20
time10 20
time30 31
原因: 函数 setTimeout
接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其它消息,setTimeout
消息必须等待其它消息处理完。
Chrome运行原理
如何展示网页
输入处理
- 用户URL框输入内容的后,UI线程会判断输入的是一个URL地址呢,还是一个query查询条件
- 如果是URL,直接请求站点资源
- 如果是query,将输入发送给搜索引擎
开始导航
- 当用户按下回车,UI线程通知网络线程发起一个网络请求,来获取站点内容
- 请求过程中,tab处于loading(转圈等待)状态
读取响应
- 网络线程接收到HTTP响应后,先检查响应头的媒体类型(MIME Type)
- 如果响应主体是一个HTML文件,浏览器将内容交给渲染进程处理
- 如果拿到的是其他类型文件,比如Zip、exe等,则交给下载管理器处理
寻找渲染进程
- 网络线程做完所有检查后,会告知主进程数据已准备完毕,主进程确认后为这个站点寻找一个渲染进程
- 主进程通过IPC消息告知渲染进程去处理本次导航
- 渲染进程开始接收数据并告知主进程自己已开始处理,导航结束,进入文档加载阶段
渲染进程 - 资源加载
- 收到主进程的消息后,开始加载HTML文档
- 除此之外,还需要加载子资源,比如一些图片,CSS样式文件以及JavaScript脚本
渲染进程 - 构建渲染树
-
构建DOM树,将HTML文本转化成浏览器能够理解的结构
-
构建CSSOM树,浏览器同样不认识CSS,需要将CSS代码转化为可理解的CSSOM
-
构建渲染树,渲染树是DOM树和CSSOM树的结合
渲染进程 - 页面布局
- 根据渲染树计算每个节点的位置和大小
- 在浏览器页面区域绘制元素边框
- 遍历渲染树,将元素以盒模型的形式写入文档流
渲染进程 - 页面绘制
- 构建图层:为特定的节点生成专用图层
- 绘制图层:一个图层分成很多绘制指令,然后将这些指令按顺序组成一个绘制列表,交给合成线程(减少绘制次数)
- 合成线程接收指令生成图块
- 栅格线程将图块进行栅格化
- 展示在屏幕上
首屏优化
- 压缩、分包、删除无用代码
- 静态资源分离
- JS脚本非阻塞加载
将JS脚本异步加载,可以减少页面的渲染阻塞,从而提高页面的加载速度。可以使用
defer
和async
等属性来实现JS的非阻塞加载 - 缓存策略
- SSR
服务器端渲染(Server Side Rendering)可以在服务器端生成HTML文档,减少客户端渲染的工作量,从而提高页面的加载速度。SSR适用于复杂的单页面应用或对SEO有要求的应用
- 预置loading、骨架屏
渲染优化
- GPU加速:一般是利用css3
- 减少回流、重绘
- 离屏渲染
将页面中的部分内容在单独的图层中进行渲染,从而减少对主渲染线程的阻塞。可以使用CSS3的
transform
和position
等属性来开启离屏渲染。 - 懒加载
JS优化
-
防止内存泄漏
-
循环尽早break
-
合理使用闭包
-
减少DOM访问
-
防抖、节流
- 防抖(debounce): 防抖是指在一定时间内,如果连续触发事件,那么只执行一次目标函数,比如LOL的回城。常用于输入框实时搜索、窗口大小调整等场景。防抖的实现原理是:设置一个定时器,在指定的延迟时间内,如果再次触发事件,则重新计时。只有在延迟时间内没有再次触发事件时,才会执行目标函数。
- 节流(throttle): 节流是指在一定时间内,无论触发多少次事件,目标函数都只执行一次,理解为技能CD。常用于滚动事件、鼠标移动等场景。节流的实现原理是:设置一个间隔时间,在这个时间内,无论事件触发多少次,都只执行一次目标函数。一旦超过这个间隔时间,就会再次执行目标函数。
总结: 防抖和节流的主要区别在于,防抖是在一定时间内只执行一次目标函数,而节流是在一定时间内控制目标函数执行次数。它们都可以有效地减少函数执行频率,降低性能开销,提高用户体验。
-
Web Worker
Web Workers是一种在后台线程中执行JavaScript代码的技术。可以将耗时的计算任务和数据处理等操作放到Web Workers中执行,避免阻塞主线程,提高页面的响应速度
跨端容器
- webview
- 小程序
- RN 、Weex
- Lynx
- Flutter
Webview
Webview,即网页视图,用于加载网页Url,并展示其内容的控件。可以内嵌在移动端App内,实现前端混合开发,大多数混合框架都是基于Webview的二次开发:比如lonic、Cordova、uniapp。
优点:
- 一次开发,处处使用,学习成本低
- 随时发布,即时更新,不用下载安装包
- 移动设备性能不断提升,性能有保障
- 通过JSBridge和原生系统交互,实现复杂功能
原生能力
Javascript调用Native
- API注入:Native获取avascript3环境上下文,对其挂载的对象或者方法进行拦截
- 使用Webview URL Scheme跳转拦截
- IOS上window.webkit.messageHandler直接通信
Native调用Javascript
- 直接通过webview暴露的API执行JS代码
- IOS webview.stringByEvaluatingJavaScriptFromString
- Android webview.evaluateJavascript
WebView< - >Nactive 通信
- JS环境中提供通信的 JSBridge
- Native端提供 SDK 响应 JSBridge 发出的调用
- 前端和客户端分别实现对应功能模块
以下是一个简单的JSBridge
JSBridge是一种在WebView中实现原生与JavaScript之间通信的技术
interface CallArgs {
callId: string; // 调用Id,唯一标识
module: string; // 调用模块
method: string; // 调用方法
data: any; // 参数
}
const Callbacks = { } // 存放回调函数 callId为key
function applyNative = (payload:CallArgs,callback:Function)=>{
const callId = prefix + callTime++
Callbacks[callId] = callback
const Args0:CallArgs = {
callId:callId,
module:payload.module || 'layout',
method:payload.method || 'randomSize',
data:payload.data
}
if(IOS){
return window.webkitURL.messageHandler.postMessage(JSON.stringify(Args0))
}else{
// 安卓对window上约定的对象进行拦截
return window.AndroidBridge(JSON.stringify(Args0))
}
}
interface ResponseArgs{
responseId: string;// 回调Id,与callId对应
errCode: number;
errMsg?: string;
data: unknown;
}
// Native 端调用webview,参数都经过序列化
const applyWebview = (res:string)=>{
const response = JSON.parse(res) as ResponseArgs
const {responseId} = response
// 从Callbacks找到对应的回调处理方法
if(typeof Callbacks[responseId]) === 'function'{
Callbacks[responseId](response)
// 回调后删除该次回调函数
delete Callbacks[responseId]
}
}
// 挂在在window上,供native直接调用
window.JSBridge = {
applyNative,
applyWebview
}
通用原理
-
UI组件
-
渲染引擎
-
逻辑控制引擎
-
通信桥梁
-
底层API抹平表面差异
思考
-
同样是基于Webview渲染,为什么小程序体验比Webview流畅?
-
未来的跨端方案会是什么?
回归Webview?(易上手)