浏览器背后的运行机制


从本章开始,我们的性能优化探险也正式进入到了“深水区”——浏览器端的性能优化。

平时我们几乎每天都在和浏览器打交道,在一些兼容任务比较繁重的团队里,苦逼的前端攻城师们甚至为了兼容各个浏览器而不断地去测试和调试,还要在脑子中记下各种遇到的 BUG 及解决方案。即便如此,我们好像并没有去主动地关注和了解下浏览器的工作原理。我想如果我们对此做一点了解,在项目过程中就可以有效地避免一些问题,并对页面性能做出相应的改进。

“知己知彼,百战不殆”,今天,我们就一起来揭开浏览器渲染过程的神秘面纱!

浏览器的“心”

浏览器的“心”,说的就是浏览器的内核。在研究浏览器微观的运行机制之前,我们首先要对浏览器内核有一个宏观的把握。

开篇我提到许多工程师因为业务需要,免不了需要去处理不同浏览器下代码渲染结果的差异性。这些差异性正是因为浏览器内核的不同而导致的——浏览器内核决定了浏览器解释网页语法的方式。
浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并没有十分明确的区分,但随着 JS 引擎越来越独立,内核也成了渲染引擎的代称(下文我们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。

目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

这里面大家最耳熟能详的可能就是 Webkit 内核了。很多同学可能会听说过 Chrome 的内核就是 Webkit,殊不知 Chrome 内核早已迭代为了 Blink。但是换汤不换药,Blink 其实也是基于 Webkit 衍生而来的一个分支,因此,Webkit 内核仍然是当下浏览器世界真正的霸主。

下面我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

开启浏览器渲染“黑盒”


什么是渲染过程?简单来说,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程(如下图)。

从这个流程来看,浏览器呈现网页这个过程,宛如一个黑盒。在这个神秘的黑盒中,有许多功能模块,内核内部的实现正是这些功能模块相互配合协同工作进行的。其中我们最需要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:

  • HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。

  • CSS 解释器:解析 CSS 文档, 生成样式规则。

  • 图层布局计算模块:布局计算每个对象的精确位置和大小。

  • 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。

  • JavaScript 引擎:编译执行 Javascript 代码。

浏览器渲染过程解析


有了对零部件的了解打底,我们就可以一起来走一遍浏览器的渲染流程了。在浏览器里,每一个页面的首次渲染都经历了如下阶段(图中箭头不代表串行,有一些操作是并行进行的,下文会说明):

  • 解析 HTML
    在这一步浏览器执行了所有的加载解析逻辑,在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。

  • 计算样式
    浏览器将识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。

  • 计算图层布局
    页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。

  • 绘制图层
    在这一步中浏览器会根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。

  • 整合图层,得到页面
    最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。

几棵重要的“树”


上面的内容没有理解透彻?别着急,我们一起来捋一捋这个过程中的重点——树!

为了使渲染过程更明晰一些,我们需要给这些”树“们一个特写:

  • DOM 树:解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。

  • CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。

  • 渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。

  • 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。

  • 绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。

基于这些“树”,我们再梳理一番:

渲染过程说白了,首先是基于 HTML 构建一个 DOM 树,这棵 DOM 树与 CSS 解释器解析出的 CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就大功告成了。

之后每当一个新元素加入到这个 DOM 树当中,浏览器便会通过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。

有心的同学可能已经在思考了,查表是个花时间的活,我怎么让浏览器的查询工作又快又好地实现呢?OK,讲了这么多原理,我们终于引出了我们的第一个可转化为代码的优化点——CSS 样式表规则的优化!

不做无用功:基于渲染流程的 CSS 优化建议


在给出 CSS 选择器方面的优化建议之前,先告诉大家一个小知识:CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看如下规则:

#myList  li {}

这样的写法其实很常见。大家平时习惯了从左到右阅读的文字阅读方式,会本能地以为浏览器也是从左到右匹配 CSS 选择器的,因此会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等于是缩小了范围后再去查找它后代中的 li 元素,没毛病。

事实上,CSS 选择符是从右到左进行匹配的。我们这个看似“没毛病”的选择器,实际开销相当高:浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList,你说坑不坑!

说到坑,不知道大家还记不记得这个经典的通配符:

* {}

入门 CSS 的时候,不少同学拿通配符清除默认样式(我曾经也是通配符用户的一员)。但这个家伙很恐怖,它会匹配所有元素,所以浏览器必须去遍历每一个元素!大家低头看看自己页面里的元素个数,是不是心凉了——这得计算多少次呀!

这样一看,一个小小的 CSS 选择器,也有不少的门道!好的 CSS 选择器书写习惯,可以为我们带来非常可观的性能提升。根据上面的分析,我们至少可以总结出如下性能提升的方案:

  • 避免使用通配符,只对需要用到的元素进行选择。

  • 关注可以通过继承实现的属性,避免重复匹配重复定义。

  • 少用标签选择器。如果可以,用类选择器替代,举个?:

false:

#myList li{}

true:

.myList_li {}
  • 不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿。举个?:

false:

.myList#title

true:

#title
  • 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。

搞定了 CSS 选择器,万里长征才刚刚开始的第一步。但现在你已经理解了浏览器的工作过程,接下来的征程对你来说并不再是什么难题~

告别阻塞:CSS 与 JS 的加载顺序优化


说完了过程,我们来说一说特性。

HTML、CSS 和 JS,都具有阻塞渲染的特性。

HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。

那么 CSS 和 JS 的阻塞又是怎么回事呢?

CSS 的阻塞

在刚刚的过程中,我们提到 DOM 和 CSSOM 合力才能构建渲染树。这一点会给性能造成严重影响:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了避免没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。

我们知道,只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。很多时候,DOM 不得不等待 CSSOM。因此我们可以这样总结:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

事实上,现在很多团队都已经做到了尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)。这个“把 CSS 往前放”的动作,对很多同学来说已经内化为一种编码习惯。那么现在我们还应该知道,这个“习惯”不是空穴来风,它是由 CSS 的特性决定的。

JS 的阻塞

不知道大家注意到没有,前面我们说过程的时候,花了很多笔墨去说 HTML、说 CSS。相比之下,JS 的出镜率也太低了点。
这当然不是因为 JS 不重要。而是因为,在首次渲染过程中,JS 并不是一个非登场不可的角色——没有 JS,CSSOM 和 DOM 照样可以组成渲染树,页面依然会呈现——即使它死气沉沉、毫无交互。

JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。

我们通过一个?来理解一下这个机制:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>JS阻塞测试</title><style>#container {background-color: yellow;width: 100px;height: 100px;}</style><script>// 尝试获取container元素var container = document.getElementById("container")console.log('container', container)</script>
</head>
<body><div id="container"></div><script>// 尝试获取container元素var container = document.getElementById("container")console.log('container', container)// 输出container元素此刻的背景色console.log('container bgColor', getComputedStyle(container).backgroundColor)</script><style>#container {background-color: blue;}</style>
</body>
</html>

三个 console 的结果分别为:

注:本例仅使用了内联 JS 做测试。感兴趣的同学可以把这部分 JS 当做外部文件引入看看效果——它们的表现一致。

第一次尝试获取 id 为 container 的 DOM 失败,这说明 JS 执行时阻塞了 DOM,后续的 DOM 无法构建;第二次才成功,这说明脚本块只能找到在它前面构建好的元素。这两者结合起来,“阻塞 DOM”得到了验证。再看第三个 console,尝试获取 CSS 样式,获取到的是在 JS 代码执行前的背景色(yellow),而非后续设定的新样式(blue),说明 CSSOM 也被阻塞了。那么在阻塞的背后,到底发生了什么呢?

我们前面说过,JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。

现在理解了阻塞的表现与原理,我们开始思考一个问题。浏览器之所以让 JS 阻塞其它的活动,是因为它不知道 JS 会做什么改变,担心如果不阻止后续的操作,会造成混乱。但是我们是写 JS 的人,我们知道 JS 会做什么改变。假如我们可以确认一个 JS 文件的执行时机并不一定非要是此时此刻,我们就可以通过对它使用 defer 和 async 来避免不必要的阻塞,这里我们就引出了外部 JS 的三种加载方式。

JS的三种加载方式

  • 正常模式:
<script src="index.js"></script>

这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。

  • async 模式:
<script async src="index.js"></script>

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。

  • defer 模式:
<script defer src="index.js"></script>

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。

通过审时度势地向 script 标签添加 async/defer,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。

小结


我们知道,当 JS 登场时,往往意味着对 DOM 的操作。DOM 操作所导致的性能开销的“昂贵”,大家可能早就有所耳闻,雅虎军规里很重要的一条就是“尽量减少 DOM 访问”。

其它前端性能优化:

  • 图片优化——质量与性能的博弈
  • 浏览器缓存机制介绍与缓存策略剖析
  • webpack 性能调优与 Gzip 原理
  • 本地存储——从 Cookie 到 Web Storage、IndexDB
  • CDN 的缓存与回源机制解析
  • 服务端渲染的探索与实践
  • 解锁浏览器背后的运行机制
  • DOM 优化原理与基本实践
  • Event Loop 与异步更新策略
  • 回流(Reflow)与重绘(Repaint)
  • Lazy-Load
  • 事件的节流(throttle)与防抖(debounce
  • 前端学习资料下载
  • 技术体系分类

前端技术架构体系(没有链接的后续跟进):

  • 调用堆栈
  • 作用域闭包
  • this全面解析
  • 深浅拷贝的原理
  • 原型prototype
  • 事件机制、
  • Event Loop
  • Promise机制、
  • async / await原理、
  • 防抖/节流原理
  • 模块化详解、
  • es6重难点、
  • 浏览器熏染原理、
  • webpack配置(原理)
  • 前端监控、
  • 跨域和安全、
  • 性能优化(参见上面性能优化相关)
  • VirtualDom原理、
  • Diff算法、
  • 数据的双向绑定
  • [TCP协议(三次握手、四次挥手)](https://blog.csdn.net/woleigequshawanyier/article/details/85223642
  • DNS域名解析

其它相关

  • 前端学习资料下载
  • 技术体系分类
  • react-native 实战项目学习
  • react-naitve 采坑笔记

欢迎各位看官的批评和指正,共同学习和成长
希望该文章对您有帮助,你的 支持和鼓励会是我持续的动力

浏览器背后的运行机制相关推荐

  1. 渲染篇二:知己知彼——解锁浏览器背后的运行机制

    知己知彼--解锁浏览器背后的运行机制 从本章开始,我们的性能优化探险也正式进入到了"深水区"--浏览器端的性能优化. 平时我们几乎每天都在和浏览器打交道,在一些兼容任务比较繁重的团 ...

  2. 区块链技术背后的运行逻辑

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 区块链技术可能是自互联网技术以来最伟大的发明.区块链可以在不需要有中央权威机构的情况下或不需要双方信任的情况下交换价值或 ...

  3. js 多个定时器_从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(二)

    作者:撒网要见鱼   https://segmentfault.com/a/1190000012925872 本文接上篇 <从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理(一)> ...

  4. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

    最近发现有不少介绍JS单线程运行机制的文章,但是发现很多都仅仅是介绍某一部分的知识,而且各个地方的说法还不统一,容易造成困惑. 因此准备梳理这块知识点,结合已有的认知,基于网上的大量参考资料,从浏览器 ...

  5. js中当等于最小值是让代码不执行_从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理...

    前言 见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会及时修正. ----------超长文+多图预警,需要花费不少时间.---------- 如果看完本文后,还对进程线程傻傻分不清,不清楚浏 ...

  6. 【Chrome浏览器插件开发】浏览器插件运行机制03之实战使用Vue.js 3 + Vite 2开发出简易的浏览器插件(含源码)

    文章目录 知识点: 一.使用 vite 创建项目 1.1 环境搭建 1.2 安装vite工具 1.3 创建vite项目 1.4 进入项目并安装依赖 1.5 修改端口 1.6 运行项目 二.创建项目资源 ...

  7. 最全面梳理 JS 运行机制解析与浏览器页面渲染的核心流程

    最近这几年,云计算的普及和 HTML5 技术的快速发展,越来越多的应用转向了浏览器 / 服务器(B/S)架构,这种改变让浏览器的重要性与日俱增,视频.音频.游戏几大核心场景也都在逐渐往 Web 使用场 ...

  8. 网络:浏览器静态资源缓存机制

    一.前言 为什么需要缓存? 缓存可以说是性能优化中简单高效的一种优化方式了.一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷. 对于一 ...

  9. ASP.NET运行原理和运行机制

    一.ASP.NET运行原理 当一个http(abbr. 超文本传输协议  hypertext transport protocol )请求发送过来并被IIS机收到之后,IIS(IIS 互联网信息服务信 ...

  10. 傻傻分不清的javascript运行机制

    学习到javascript的运行机制时,有几个概念经常出现在各种文章中且容易混淆.Execution Context(执行环境或执行上下文),Context Stack (执行栈),Variable ...

最新文章

  1. 详解Paint的setPathEffect(PathEffect effect)
  2. SAP WM 针对采购订单收货时候不能自动获取物料主数据里的Special Movement Indicator?
  3. 标号的类型是near还是far有什么区别,作用是什么?
  4. 跟alex学python_跟着Alex学习python
  5. [leetcode] 根据String数组构造TreeNode,用于LeetCode树结构相关的测试用例
  6. Windows 如何在命令终端(CMD)使用命令来访问本地/远程的 Oracle 数据库呢?
  7. 深入理解卷积层,全连接层的作用意义
  8. 把文本框的值转换成Image
  9. 20191122 视频版控制台上的极乐净土
  10. netapp linux ntfs,netapp存储常用命令
  11. 063.django之模板层
  12. 如何用ABP框架快速完成项目 - 自动化测试 - 前端angular e2e protractor
  13. friends105. The One with the East German Laundry Detergent
  14. linux cp指令报错:cp: -r not specified; cp: omitting directory ‘xxx‘(需要加-r递归拷贝)
  15. 什么是敏捷管理 常用的敏捷Scrum会议有哪些
  16. JAVA毕业设计江西婺源旅游文化推广系统计算机源码+lw文档+系统+调试部署+数据库
  17. mac 命令行安装软件
  18. (一)mysql 运维基础篇(Linux云计算从入门到精通)
  19. 使用vlc串流http视频链接
  20. 美国约翰斯·霍普金斯大学全球新冠疫情统计数据网址

热门文章

  1. java中solr的面试题_SOlR面试题
  2. convertTO函数 简介
  3. 关于土地分类格式互转、土地利用转移矩阵、变化图谱计算详解
  4. 【深度学习之Tensorflow2.0】函数matmul和函数multiply的用法
  5. Jmeter基础教程
  6. 实时系统动态内存算法分析dsa(二)——TLSF代码分析
  7. 72张三国历史演变地图
  8. 2007上半年网络游戏企业报告总结
  9. ubuntu下QQ无法登录解决。
  10. Ubuntu中下载安装NVIDIA显卡驱动