写点什么

现代浏览器原理

用户头像
roadup
关注
发布于: 2021 年 01 月 10 日

本文讲解浏览器基本原理, 以 Chrome 为例, 概览浏览器全貌, 了解浏览器背后的工作机制, 为更深入的前端开发作准备.


进程和线程

如何区分进程和线程

进程有自己独立的资源,包括内存、堆栈等, 进程之间相互独立. 线程属于进程, 一个进程中至少包含一个线程, 一个进程中的多个线程共享进程的资源.


在电脑中可以打开任务管理器( mac 中叫活动监视器 ),, 可以看到一个进程列表. 同时还能看到进程对系统资源的占用情况


image.png


或者使用命令行工具 htop 查看


image.png


进程是 CPU 资源分配的最小单位.线程是 CPU 调度的最小单位, 线程是建立在进程基础上的一次程序运行单位.


不同进程之间也可以通信, 不过代价比较大.


现在通常所说的单线程和多线程,都是指的一个进程内的.

浏览器是多进程的

浏览器是多进程的, 通常来说一个页面对应这一个进程

浏览器之所以能够运行, 是因为操作系统给它提供了所需要的资源, 如内存、 CPU 等

浏览器中也包含一个类似于任务管理器的工具, 在 Chrome 中可以通过菜单->更多工具->任务管理器打开.


image.png


在 Chrome 中, 每个 Tab 页面都有一个独立的进程, 另外浏览器还有一个主进程, Browser 进程, 但浏览器有自己的优化机制, 有些进程会被合并, 比如多个新 Tab 页面会被合并到一个进程中,  插件的后台和前台会合并在一个进程中等.

浏览器包含的线程

  1. Browser 进程: 浏览器的主进程, 负责协调和控制浏览器, 只有一个.

  2. 负责浏览器界面的显示, 与用户交互, 如前进后退等

  3. 负责各个页面的管理, 创建和销毁其他进程

  4. 将 Renderer 进程得到的内存中的图片绘制到用户界面上

  5. 网络资源的管理、下载等

  6. 第三方插件进程: 每个插件对应一个进程, 仅当使用该插件时才会创建

  7. GPU 进程: 最多一个, 用于 3D 图像的绘制

  8. 浏览器渲染进程:  也是浏览器内核进程、 Renderer 进程, 通常每个页面一个进程, 互相隔离, 用于页面渲染、执行脚本、处理各种事件等.


多进程的优势

相比于单进程浏览器,多进程浏览器

  • 可以避免单个页面崩溃影响整个浏览器

  • 可以避免第三方插件崩溃影响整个浏览器

  • 可以充分利用多核心处理器的优势

  • 方便使用沙盒模型隔离插件等进程, 提升浏览器的稳定性

但多进程的浏览器内存资源消耗比较大. 现代浏览器已实现自动的进程管理, 会根据不同的的及其性能环境实现部分进程的合并, 以减少资源的消耗

浏览器内核

浏览器内核进程也是 Renderer 进程.

可以认为页面的渲染、JS 的运行、事件循环都在这个进程内

浏览器的渲染进程是多线程的, 内核进程中通常包含以下线程:

  • GUI 线程

  • 负责渲染页面, 解析 HTML、CSS 构建 DOM 树和 RenderObject 结构, 布局和绘制等

  • 页面需要重绘或回流时, 该线程就会执行

  • GUI 线程与 JS 引擎线程互斥.

  • JS 引擎线程, JS 内核

  • 负责执行 JS 脚本, 如 v8

  • 解析 JavaScript 脚本, 执行代码

  • 每个 Tab 页只有一个 JS 线程

  • 事件线程

  • 归属于浏览器而不是 JS 引擎, 用来控制事件循环

  • 当遇到需要异步执行的代码时, 会将相应的事件添加到事件队列中

  • 定时器线程

  • setTimeout 和 setInterval 所在线程

  • 浏览器的定时器并不是由 JS 引擎计数的, 因为 JS 引擎是单线程, 如果处于阻塞状态会影响计时的准确性

  • 计时完毕后,定时器线程会将事件添加到事件队列中,等待 JS 引擎空闲后执行

  • W3C 规范要求 setTimeout 的最小时间间隔为 4ms

  • HTTP 请求线程

  • 用于开启 HTTP 请求

  • 当检测到请求状态变更时, 如果有设置回调函数,则会产生相应的事件.



Browser 进程和浏览器内核进程的通信

当我们打开一个页面时, Brewser 进程收到我们的请求,  首先需要获取页面的内容, 随后将该任务通过 RendererHost 接口传递给 Render 进程.

Renderer 进程收到接口消息后, 进行简单解释, 然后交给渲染进程, 之后开始渲染

  • 渲染进程接收请求, 加载网页并渲染网页, 这其中可能需要 Browser 进程获取资源和 GPU 进程帮助渲染

  • 还有可能会有 JS 线程操作 DOM, 可能引发回流重绘

  • 最后渲染进程将结果传递给 Browser 进程


Browser 进程接收到结果后将页面显示出来

浏览器内核中线程的关系

GUI 渲染线程与 JS 引擎线程互斥

由于 JavaScript 可以操作 DOM, 如果在修改元素的同时渲染页面, 那么渲染前后元素的数据可能不一致. 为了防止渲染出现不可预期的结果, 浏览器设置了 GUI 和 Js 引擎互斥的关系, 当其中一个执行的时候, 另外一个会被挂起.

JS 阻塞页面

当 JS 长时间执行的时候, 由于 GUI 线程被挂起, 此时就算 GUI 有更新,也会被保存在队列中,等待 JS 空闲后再渲染. 如果 JS 由于大量的计算, 需要很长时间才能空闲的话, GUI 就会长时间不能更新页面, 自然就感觉页面卡顿. 因此要避免 JS 长时间占用, 导致页面卡顿.


React 在更新过程中会有大量的计算,  因此 React 在 16 版本之后使用了异步渲染的方式, 让更新过程可以暂停, 使得 GUI 能有机会更新页面, 从而防止页面卡顿.

WebWorker, JS 的多线程

为了让 JS 能应对 CPU 密集型计算任务,  在 HTML5 中加入了 WebWorker.

Web Worker 为内容在后台线程中运行脚本提供了一种简单的方法. 线程可以执行任务而不干扰用户界面. 此外也可以使用网络请求执行 I/O. 一旦创建, 一个 worker 可以将消息发送到它的父线程, 反之亦然.

worker 运行在另一个全局上下文中, 不同于主线程的 window, 因此在 worker 中使用 window 将会返回错误


创建 worker 时, JS 引擎向浏览器申请新开一个子线程( 子线程是浏览器开的, 完全受主线程控制, 并且不能操作 dom ).

JS 引擎线程与 worker 线程间通过特定的方式通信, 需要序列化对象来与线程进行特定数据的交互.


虽然我们可以通过 web worker 来开启新的线程, 但 JS 引擎的单线程本质并没有改变.

WebWorker 和 SharedWorker

区别

WebWorker 只属于某个页面, 不会和其他页面的进程共享

SharedWorker 是所有页面共享的, 属于浏览器进程

浏览器渲染流程

浏览器内核拿到请求的内容后, 大致可以划分为几个步骤

  1. 解析 html 建立 dom 树

  2. 解析 css 构建 render 树

  3. 计算 render 布局,  负责计算各个元素的尺寸、位置等

  4. 绘制页面, 计算每个像素的信息

  5. 将各个图层进行合成, 显示在屏幕上

渲染完成后就是触发 load 事件, 执行 js 脚本


image.png


load 事件与 DOMContentLoaded

当 DOM 加载完成,  会触发 DOMCOntentLoaded 事件,  不包括样式表 图片等.

当页面上的所有的 DOM、 样式、 脚本、图片都加载完成时, 会触发 load 事件


DOMContentLoaded事件一定早于onload吗???

css 加载与 Dom 树渲染

css 加载不会阻塞 DOM 树的解析, 加载过程中 DOM 正常构建, 但会阻塞 render 树的渲染, 因为 render 树的构建是依赖 css.

如果不阻塞的话, 当 css 加载完成后,render 树可能需要需要重绘甚至回流, 造成没有必要的损耗.

普通图层和复合图层

浏览器渲染的图层包含两类, 普通图层和复合图层

普通文档流可以理解为一个复合图层(默认复合层), 使用 absolute 、fixed 定位的元素通常也在这个图层里.

当我们使用硬件加速的方式声明一个新的复合图层时, 这个图层后单独分配资源, 这个图层里面引起的回流重绘不会影响默认复合层.

可以认为: GPU 中, 各个图层是单独绘制的, 所以互不影响. 这也是为什么某些场景下硬件加速的效果非常好


可以在 Chrome 的调试工具中找到图层工具, 查看页面元素的图层信息


image.png


硬件加速

将该元素变成一个复合图层,  就是传说的硬件加速技术

  • 最常用的方式: translate3d, translateZ

  • opacity 属性/过渡动画 需要执行的过程中才会创建复合层, 动画没有开始或结束后还会回到之前的状态

  • will-change 属性, 一般配合 opacity 与 translate 使用. 作用是提前告诉浏览器这个元素可能会变化, 浏览器会提前做一下优化工作

  • video/iframe/canvas/webgl 等元素

  • 其它一些插件, 比如 flash


absolute 和硬件加速的区别

absolute 虽然可以脱离普通文档流, 当没有脱离默认复合层

所以, 即使 absolute 的信息改变不会改变普通文档流中 render 树, 但是, 浏览器最终绘制时, 是整个复合层绘制的, 而硬件加速就直接在另一个复合层绘制, 因此这个复合层的绘制不会影响默认复合层, 仅仅引起最后的合成.


复合图层的作用

一般一个元素开启硬件加速后会变成复合图层, 可以独立于普通文档流中, 改变后可以避免整个页面的重绘, 提升性能.

但是尽量不要大量使用复合图层, 否则由于资源消耗过渡, 页面反而会变得更卡.


硬件加速时请使用 index

使用硬件加速时, 尽可能使用 index, 防止浏览器默认给后续元素创建复合层渲染

如果这个元素添加了硬件加速, 并且 index 层级比较低, 那么这个元素的后面其他元素, 会变成复合层渲染, 如果处理不当会引起极大的性能问题.

简单来说, 如果 A 是一个复合图层, 并且 B 在 A 上面, 那么 B 也会被隐式转为一个复合图层.

http://web.jobbole.com/83575/

JS 运行机制

  • JS 引擎

  • 事件线程

  • 定时器线程


JS 分为同步任务和异步任务

同步任务都在主线程上执行, 形成一个执行栈

主线程之外, 事件触发线程管理这一个任务对列了,只要异步任务有了运行结果, 就在任务队列字中放置一个事件. 一旦执行栈中所有的同步任务执行完毕, 系统就会读取任务队列, 将可运行的异步任务添加到执行栈中, 开始执行.


这也可以解释了为什么浏览器定时器是有误差的了, 因为定时器结束时只是将任务推送到事件队列中, 此时的执行栈还有别的任务, 因此需要等到这些任务完成之后, 才会开始执行定时器的任务.

事件循环


主线程执行时会产生执行栈, 栈中的代码在产生异步任务时, 会在任务队列中添加各种事件, 如定时器, 网络请求等

栈中的代码执行完毕时, 会从任务队列中获取任务来执行,继而产生循环.

定时器

事件循环机制的核心是 Js 引擎线程和事件触发线程

定时器是有定时器线程控制的, 当调用 setTimeout 或 setInterval 时, 计时器就开始计时, 计时完成后会将指定的回调添加到任务队列中, 等待主线程执行.


用 setTimeout 模拟定期计时和直接使用 setInterval 是有区别的,  因为每次 setTimeout 计时结束后会执行代码, 然后才会设定新的计时器. 中间会有误差. 而 setInterval 则每次都会在精确的之后推入事件, 当事件的执行就不一定准确了, 有可能上一个事件还没执行, 下一个事件就来了. 这就是 setInterval 的累积效应, 会导致代码执行很多次, 而且之间没有间隔.


因此, 一般会建议使用 setTimeout 模拟 setInterval, 或者在做动画时使用 requestAnimationFrame.

微任务与宏任务

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/


宏任务: 可以理解是每次执行栈的代码就是一个宏任务. 每一个 task 会从头到尾执行, 不会执行其他的代码. 浏览器为了能够使得 JS 内部 task 与 DOM 任务能有序的执行,会在一个 task 执行结束后, 下一个 task 执行之前, 对页面进行想重新渲染.

常见的宏任务有 setTimeout、 setInterval、MessageChannel, 并且 MessageChannel 的优先级大于 setTimeout

宏任务是由事件线程维护


微任务: 可以理解为在当前 task 结束后立即执行的任务, 也就是说, 在当前 task 任务后, 下一个 task 之前执行, 因此它比 setTimeout 会更快, 因为无序等待渲染.

常见的微任务有 Promise、process.nextTick ( 在 node 环境下, process.nextTick 的优先级会高于 Promise, 先于 Promise 执行 )

微任务由 JS 引擎维护


官方的 Promise 是 micro task, polyfill 版本是宏任务, 是通过 setTimeout 模拟的


image.png


浏览器执行过程


https://www.yuque.com/roadup/frontend/io2zp2

发布于: 2021 年 01 月 10 日阅读数: 1493
用户头像

roadup

关注

君子知命不惧,日日自新 2020.05.20 加入

还未添加个人简介

评论 (3 条评论)

发布
用户头像
牛啤
2021 年 01 月 15 日 23:18
回复
用户头像
赞!
2021 年 01 月 14 日 11:12
回复
用户头像
good
2021 年 01 月 13 日 16:25
回复
没有更多了
现代浏览器原理