作者 | zhangwang来源 | https://zhuanlan.zhihu.com/p/47407398可能每一个前端工程师都想要理解浏览器的工作原理。我们希望知道从在浏览器地址栏中输入 url 到页面展现的短短几秒内浏览器究竟做了什么;我们希望了解平时常常听说的各种代码优化方案是究竟为什么能起到优化的作用;我们希望更细化的了解浏览器的渲染流程。浏览器的多进程架构
一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此,以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。对一些前端开发同学来说,进程和线程的概念可能会有些模糊,为了更好的理解浏览器的多进程架构,这里我们简单讨论一下进程和线程。进程(process)和线程(thread)
进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情。浏览器的架构
有了上面的知识做铺垫,我们可以更合理的讨论浏览器的架构了,其实如果要开发一个浏览器,它可以是单进程多线程的应用,也可以是使用 IPC 通信的多进程应用。
不同浏览器的架构模型
Chrome 的不同进程Browser Process:
负责包括地址栏,书签栏,前进后退按钮等部分的工作;
负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;
Renderer Process:
负责一个 tab 内关于网页呈现的所有事情
Plugin Process:
负责控制一个网页用到的所有插件,如 flash
GPU Process
负责处理 GPU 相关的任务
不同进程负责的浏览器区域示意图
Chrome 任务管理器面板Chrome 多进程架构的优缺点
优点某一渲染进程出问题不会影响其他进程
更为安全,在系统层面上限定了不同进程的权限
测试了一下在 Chrome 中打开不断打开知乎首页,在 Mac i5 8g 上可以启动四十多个渲染进程,之后新打开 tab 会合并到已有的渲染进程中。Chrome 把浏览器不同程序的功能看做服务,这些服务可以方便的分割为不同的进程或者合并为一个进程。以 Broswer Process 为例,如果 Chrome 运行在强大的硬件上,它会分割不同的服务到不同的进程,这样 Chrome 整体的运行会更加稳定,但是如果 Chrome 运行在资源贫瘠的设备上,这些服务又会合并到同一个进程中运行,这样可以节省内存,示意图如下。不同性能的硬件的不同进程划分iframe 的渲染 — Site Isolation在上面的进程图中我们还可以看到一些进程下还存在着 Subframe,这就是 Site Isolation 机制作用的结果。Site Isolation 机制从 Chrome 67 开始默认启用。这种机制允许在同一个 Tab 下的跨站 iframe 使用单独的进程来渲染,这样会更为安全。
iframe 会采用不同的渲染进程Site Isolation 被大家看做里程碑式的功能, 其成功实现是多年工程努力的结果。Site Isolation 不是简单的叠加多个进程。这种机制在底层改变了 iframe 之间通信的方法,Chrome 的其它功能都需要做对应的调整,比如说 devtools 需要相应的支持,甚至 Ctrl + F 也需要支持。关于 Site Isolation 的更多内容可参考下述链接https://developers.google.com/web/updates/2018/07/site-isolationdevelopers.google.com介绍完了浏览器的基本架构模式,接下来我们看看一个常见的导航过程对浏览器来说究竟发生了什么。
导航过程发生了什么
也许大多数人使用 Chrome 最多的场景就是在地址栏输入关键字进行搜索或者输入地址导航到某个网站,我们来看看浏览器是怎么看待这个过程的。我们知道浏览器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又对这些工作进一步划分,使用不同线程进行处理:UI thread :控制浏览器上的按钮及输入框;
network thread: 处理网络请求,从网上获取数据;
storage thread: 控制文件等的访问;
浏览器主进程中的不同线程
UI thread 通知 Network thread 加载相关信息
判断响应内容的格式

由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。确认导航进过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。
Browser Process 和 Renderer Process 通过 IPC 通信,请求 Renderer Process 渲染页面
Renderer Process 发送 IPC 消息通知 browser process 页面已经加载完成beforeunload 事件,这个事件再次涉及到 Browser Process 和 renderer Process 的交互,当当前页面关闭时(关闭 Tab ,刷新等等),Browser Process 需要通知 renderer Process 进行相关的检查,对相关事件进行处理。
浏览器进程发送 IPC 消息给渲染进程,通知要离开当前网站了window.location = http://newsite.com ) renderer process 会首先检查是否有 beforeunload 事件处理器,导航请求由 renderer process 传递给 Browser process如果导航到新的网站,会启用一个新的 render process 来处理新页面的渲染,老的进程会留下来处理类似 unload 等事件。关于页面的生命周期,更多内容可参考 Page Lifecycle API 。
Service Worker 依据具体情形做处理渲染进程是如何工作的
渲染进程几乎负责 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:
渲染进程包含的线程主线程 Main thread
工作线程 Worker thread
排版线程 Compositor thread
光栅线程 Raster thread
构建 DOM
加载次级的资源
![]()
等标签,preload scanner 会把这些请求传递给 Browser process 中的 network thread 进行相关资源的下载。JS 的下载与执行
标签时,渲染进程会停止解析 HTML,而去加载,解析和执行 JS 代码,停止解析 html 的原因在于 JS 可能会改变 DOM 的结构(使用诸如 documwnt.write()等API)。不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在 标签上添加了 async 或 defer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。更多的方法可参考 Resource Prioritization – Getting the Browser to Help You样式计算
渲染进程主线程计算每一个元素节点的最终样式值获取布局
display:none ,这个元素不会出现在布局树上,伪元素虽然在 DOM 树上不可见,但是在布局树上是可见的。
主线程遍历 DOM 及 对应元素的样式,构建出布局树绘制各元素
主线程依据布局树构建绘制记录合成帧
will-change CSS 属性的元素,会被看做单独的一层,
主线程遍历布局树生成层树will-change,不过组合过多的层也许会比在每一帧都栅格化页面中的某些小部分更慢。为了更合理的使用层,可参考 坚持仅合成器的属性和管理层计数 。一旦层树被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层的可以达到整个页面的大小,因此,合成器线程将它们分成多个磁贴,并将每个磁贴发送到栅格线程,栅格线程会栅格化每一个磁贴并存储在 GPU 显存中。
合成器线程会发送合成帧给 GPU 渲染浏览器对事件的处理
浏览器通过对不同事件的处理来满足各种交互需求,这一部分我们一起看看从浏览器的视角,事件是什么,在此我们先主要考虑鼠标事件。在浏览器的看来,用户的所有手势都是输入,鼠标滚动,悬置,点击等等都是。当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。事件发生时,浏览器进程会发送事件类型及相应的坐标给渲染进程,渲染进程随后找到事件对象并执行所有绑定在其上的相关事件处理函数。
事件从浏览器进程传送给渲染进程
document.body.addEventListener('touchstart',
event=>{ if(event.target === area){ event.preventDefault(); }}
);上述做法很常见,但是如果从浏览器的角度看,整个页面都成了 non-fast scrollable region 了。这意味着即使操作的是页面无绑定事件处理器的区域,每次输入时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。
由于事件绑定在最顶部,整个页面都成为了 non-fast scrollable regionpassive: true 做为参数,这样写就能让浏览器即监听相关事件,又让组合器线程在等等主线程响应前构建新的组合帧。document.body.addEventListener('touchstart',
event=>{ if(event.target === area){ event.preventDefault() } },{passive:true}
);不过上述写法可能又会带来另外一个问题,假设某个区域你只想要水平滚动,使用 passive: true 可以实现平滑滚动,但是垂直方向的滚动可能会先于event.preventDefault()发生,此时可以通过 event.cancelable 来防止这种情况。document.body.addEventListener('pointermove',event=>{ if(event.cancelable){ event.preventDefault();// block the native scroll /* * do what you want the application to do here */ } },{passive:true});也可以使用css属性 touch-action 来完全消除事件处理器的影响,如:#area { touch-action: pan-x; }查找到事件对象当组合器线程发送输入事件给主线程时,主线程首先会进行命中测试(hit test)来查找对应的事件目标,命中测试会基于渲染过程中生成的绘制记录( paint records )查找事件发生坐标下存在的元素。
主线程依据绘制记录查找事件相关元素事件的优化
一般我们屏幕的刷新速率为 60fps,但是某些事件的触发量会不止这个值,出于优化的目的,Chrome 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行 。而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非连续性事件则会立即被触发。
Chrome 会合并连续事件到下一帧触发getCoalescedEvents API 来获取组合的事件。示例代码如下:window.addEventListener('pointermove',event=>{ const events =event.getCoalescedEvents(); for(letevent of events){ const x =event.pageX; const y =event.pageY; // draw a line using x and y coordinates. }});
通过 getCoalescedEvents API 获取到每一个事件相关参考链接
CPU, GPU, Memory, and multi-process architecture
What happens in navigation
Inner workings of a Renderer Process
Input is coming to the Compositor
浏览器的工作原理:新式网络浏览器幕后揭秘


本文详细介绍了浏览器的多进程架构,包括Browser Process、Renderer Process、Plugin Process和GPU Process及其职责。Chrome的多进程架构提高了安全性且避免了单一进程故障影响整个浏览器。浏览器导航过程涉及UI、网络和存储线程。渲染进程主要任务由主线程、工作线程、排版线程和光栅线程完成,包括DOM构建、资源加载、JS执行和绘制。浏览器还优化了事件处理以提升性能。
1610

被折叠的 条评论
为什么被折叠?



