浏览器内核、JS引擎及其工作原理

浏览器(Web Browser,网页浏览器)是一种用来检索、展示以及传递 Web 信息资源的应用程序。Web 信息资源由统一资源标识符(Uniform Resource Identifier,URI)所标记,它是一张网页、一张图片、一段视频或者任何在Web上所呈现的内容。使用者可以借助超级链接( Hyperlinks),通过浏览器浏览互相关联的信息。

浏览器发展历程

世界上第一个浏览器

Tim Berners-Lee(蒂姆·伯纳斯·李) 于1990年发明了第一个网页浏览器 WorldWideWeb,后来为了避免和 World Wide Web 命名冲突而改叫 Nexus。这时浏览器的功能很简单,只支持文本、简单的样式表和有限格式的图片和声音。

Erwise 是第一个普遍可用的使用 GUI 的网页浏览器,由罗伯特·卡里奥发起开发。

第一次浏览器大战

1993年,马克·安德森 发布 Mosaic——“世界上最流行的浏览器”,进一步推动了浏览器的创新,这使得万维网更易于使用。安德森的浏览器引发了1990年代的互联网热潮。安德森是 NCSA 中 Mosaic 团队的领导者,他不久后辞职并成立了自己的公司—— Netscape,发布了受 Mosaic 影响的 Netscape Navigator。Netscape Navigator 很快便成为世界上最流行的浏览器,市占率一度达到90%。

作为应对,错失了互联网浪潮的微软匆促购入了 Spyglass 公司的技术,发布 Internet Explorer。这引发了第一轮浏览器大战。因捆绑于 Windows,Internet Explorer 于网页浏览器市场获取了主导地位,其市占率于2002年达到最高时超过95%。

第二次浏览器大战

由于微软推出的 IE 的影响, 网景公司的 Netscape 每况日下。但网景公司并没有坐以待毙,在 1998 年 成立了 Mozilla 基金会,并准备开发新的浏览器 。

2003 年,苹果公司搭配自家的 Mac OS X Panther 推出了 Safari浏览器,受限于 Mac 的用户量,Safari 并未产生多大的影响;2005 年,苹果开源了 Safari 的内核(渲染引擎)—— 大名鼎鼎的 Webkit,意义之重大,不言而喻。

2004 年,网景公司发布了全新的浏览器 Firefox,并搭配了 Gecko 内核。它功能丰富,支持用户拓展,一推出就深受大家喜爱,市场份额也稳步上升。

2008 年,Google 公司以苹果的开源项目 Webkit 作为内核,创建了一个全新的项目 Chromium 浏览器。它被谷歌拿来做 Web 技术试验场,尝试一些大胆创新的技术。同年,推出了面向用户的浏览器 —— Chrome,它会选择 Chromium 的稳定版本为基础,集成一些私有的编码解码器。或许你会疑惑,既然推出了 Chromium,为什么还要推出 Chrome?其实很简单,Chromium 是面向开发者玩的,Chrome 是选取 Chromium 的稳定版面向普通大众的,二者并不冲突。

自此,微软 IE 依靠 Windows 系统依然占据重要地位,Mozilla 火狐 和 Google Chrome 二者也拥有大量粉丝,逐步谗食 IE 的市场份额,形成三足鼎立之势。

2010 ~ 现在

2010 年,苹果宣布了 Webkit2 ,改为多进程的结构模型。 2013 年,Google 和 苹果 出现了对内核发展的分歧。于是 4 月份,Google 宣布了全新的内核项目 —— Blink。该内核早期是完全复制了 Webkit ,之后删除了与 Chromium 无关的代码,并开始大刀阔斧地对内核进行改革。 2015 年,随着 Windows 10 的发布,微软公司宣布了全新的浏览器 —— Microsoft Edge,用来取代 IE 浏览器,一代王者逐步退出舞台。 2017 年,Mozilla 宣布了为 Firefox 全新的打造的 Quantum 版本,号称新时代的最快浏览器。 目前为止,Google Chrome 以绝对的优势远远领先于其他浏览器,并且对 Web 技术 依然充满热情,以优秀的特性和性能吸引了绝大部分开发者。

浏览器的主要组件

简单来说浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。

shell 是指浏览器的外壳:例如菜单,工具栏等。主要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种功能的。

内核才是浏览器的核心:内核是基于标记语言显示内容的程序或模块。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。

  • 用户界面:包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  • 浏览器引擎:在用户界面和呈现引擎之间传送指令。
  • 呈现引擎:也称浏览器内核,负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  • 网络:用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  • 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  • Javascript解析器:用于解析和执行 JavaScript 代码。
  • 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

浏览器最重要的部分是浏览器的内核。浏览器内核是浏览器的核心,也称“渲染引擎”,用来解释网页语法并渲染到网页上。浏览器内核决定了浏览器该如何显示网页内容以及页面的格式信息。不同的浏览器内核对网页的语法解释也不同,因此网页开发者需要在不同内核的浏览器中测试网页的渲染效果。

浏览器内核又可以分成两部分:渲染引擎和 JS 引擎。

渲染引擎

渲染引擎负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。

内核的种类很多,如加上没什么人使用的非商业的免费内核,可能会有 10 多种,但是常见的浏览器内核可以分这四种:Trident、Gecko、Blink、Webkit。

Trident

该内核程序在 1997 年的 IE4 中首次被采用,是微软在 Mosaic 代码的基础之上修改而来的,并沿用到 IE11,也被普遍称作 IE内核

由于其被包含在全世界使用率最高的操作系统 Windows 中,得到了极高的市场占有率,从而使得 Trident 内核(也被称为 IE 内核)长期一家独大,微软也很长时间都并没有更新 Trident 内核。这导致了两个后果:

  • 一是 Trident 内核曾经几乎与 W3C 标准脱节(2005年)。
  • 二是 Trident 内核的大量 Bug 等安全性问题没有得到及时解决。

再加上一些致力于开源的开发者和一些学者们公开自己认为 IE 浏览器不安全的观点,也有很多用户转向了其他浏览器,Firefox 和 Opera 就是这个时候兴起的。

注:IE 从版本 11 开始,初步支持 WebGL 技术。IE8 的 JavaScript 引擎是 Jscript,IE9 开始用 Chakra,这两个版本区别很大,Chakra 无论是速度和标准化方面都很出色。

Window10 发布后,IE 将其内置浏览器命名为 Edge,Edge 最显著的特点就是新内核 EdgeHTML。

Gecko

Netscape6 开始采用的内核,后来的 Mozilla FireFox 浏览器也采用了该内核,Gecko 的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能。因为这是个开源内核,因此受到许多人的青睐,Gecko 内核的浏览器也很多,这也是 Gecko 内核虽然年轻但市场占有率能够迅速提高的重要原因。

Gecko 内核的浏览器以 Firefox 用户最多,所以有时也会被称为 Firefox 内核。此外 Gecko 也是一个跨平台内核,可以在Windows、 BSD、Linux 和 Mac OS X 中使用。

Webkit

Safari 是苹果公司开发的浏览器,使用了KDE(Linux桌面系统)的 KHTML 作为浏览器的内核,Safari 所用浏览器内核的名称是大名鼎鼎的 WebKit。 Safari 在 2003 年 1 月 7 日首度发行测试版,并成为 Mac OS X v10.3 与之后版本的默认浏览器,也成为苹果其它系列产品的指定浏览器(也已支持 Windows 平台)。

如上述可知,WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。当年苹果在比较了 Gecko 和 KHTML 后,选择了后者来做引擎开发,是因为 KHTML 拥有清晰的源码结构和极快的渲染速度。

Webkit 内核可以说是以硬件盈利为主的苹果公司给软件行业的最大贡献之一。

注:苹果在 Safari 里面使用了自己的 Nitro JavaScript 引擎(只用 WebKit 来渲染 HTML),所以一般说到 Webkit,通常指的就是渲染引擎(而不包括 Javascript 引擎)。

2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium。

chromium 是基于 webkit 引擎的,却把 WebKit 的代码梳理得可读性提高很多。因此 Chromium 引擎和其它基于 WebKit 的引擎所渲染页面的效果也是有出入的。所以有些地方会把 chromium 引擎和 webkit 区分开来单独介绍,而有的文章把 chromium 归入 webkit 引擎中,都是有一定道理的。

Blink 是一个由 Google 主导开发的开源浏览器引擎,Google 计划将这个渲染引擎作为 Chromium 计划的一部分,并且在2013年4月的时候公布了这一消息。

webkit 用的好好的,为何要投入到一个新的内核中去呢?

Blink 其实是 WebKit 的分支,如同 WebKit 是 KHTML 的分支。Google 的 Chromium 项目此前一直使用 WebKit 作为渲染引擎,但出于某种原因,并没有将其多进程架构移植入Webkit。

后来,由于苹果推出的 WebKit2 与 Chromium 的沙箱设计存在冲突,所以 Chromium 一直停留在 WebKit,并使用移植的方式来实现和主线 WebKit2 的对接。这增加了 Chromium 的复杂性,且在一定程度上影响了 Chromium 的架构移植工作。

基于以上原因,Google 决定从 WebKit 衍生出自己的 Blink 引擎(后由 Google 和 Opera Software 共同研发),将在 WebKit 代码的基础上研发更加快速和简约的渲染引擎,并逐步脱离 WebKit 的影响,创造一个完全独立的 Blink 引擎。

总的来说,Chromium 基于 Webkit 引擎,衍生出 Blink。据说 Blink 删除了 880w 行 webkit 代码。

Presto

Presto 是 Opera 自主研发的渲染引擎,然而为了减少研发成本,Opera 在 2013 年 2 月宣布放弃 Presto,转而跟随 Chrome 使用 WebKit 分支的 Chromium 引擎作为自家浏览器核心引擎。

在 Chrome 于 2013 年推出 Blink 引擎之后,Opera 也紧跟其脚步表示将转而使用 Blink 作为浏览器核心引擎。

Opera 的一个里程碑作品是 Opera7.0,因为它使用了 Opera Software 自主开发的 Presto 渲染引擎,取代了旧版 Opera 4 至 6 版本使用的 Elektra 排版引擎。该款引擎的特点就是渲染速度的优化达到了极致,然而代价是牺牲了网页的兼容性

换内核的代价对于 Opera 来说过于惨痛。使用谷歌的 WebKit 内核之后,原本快速,轻量化,稳定的 Opera 浏览器变得异常的卡顿,而且表现不稳定,Opera 原本旧内核浏览器书签同步到新内核上的工作 Opera 花了整整两年时间,期间很多 Opera 的用户纷纷转投谷歌浏览器和其他浏览器,造成了众多的用户流失。

JS 引擎

JS 引擎则是解析 Javascript 语言,执行 javascript 语言来实现网页的动态效果。

最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎

主流浏览器的 JavaScript 引擎:

  • V8:用 C++ 编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
  • JavaScriptCore:开放源代码,用于 webkit 型浏览器,如:Safari。2008年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为 Nitro 的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
  • Rhino:由Mozilla基金会管理,开放源代码,完全以Java编写,用于 HTMLUnit。
  • SpiderMonkey:第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于Mozilla Firefox。
  • Chakra(JScript引擎):用于Internet Explorer 11。
  • Chakra (JavaScript引擎)用于Microsoft Edge。
  • KJS:KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波顿开发,用于 KDE 项目的 Konqueror 网页浏览器中。

主流浏览器

国内常见的浏览器有:IE、Firefox、QQ浏览器、Safari、Opera、Google Chrome、百度浏览器、搜狗浏览器、猎豹浏览器、360浏览器、UC浏览器、遨游浏览器、世界之窗浏览器等。但目前最为主流浏览器有五大款:

  • IE
  • Firefox
  • Google Chrome
  • Safari
  • Opera

image-20200903001648587

各常用浏览器所使用的内核:

  • IE:Trident 内核
  • Chrome:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核
  • Firefox:Gecko 内核
  • Safari:Webkit 内核
  • Opera:最初是自己的 Presto 内核,后来是 Chromium 内核,现在是 Blink 内核
  • 360、猎豹浏览器:Trident + Chrome 双内核
  • 搜狗、遨游、QQ浏览器:Trident(兼容模式)+ Webkit(高速模式)
  • 百度、世界之窗浏览器:Trident 内核
  • 2345:以前是IE内核,现在也是 IE + Chrome 双内核
浏览器 内核(渲染引擎) JavaScript 引擎
Chrome Blink(28~) Webkit(Chrome 27) V8
FireFox Gecko SpiderMonkey
Safari Webkit JavaScriptCore
Edge EdgeHTML Chakra(for JavaScript)
IE Trident Chakra(for JScript)
PhantomJS Webkit JavaScriptCore
Node.js - V8

浏览器的工作原理

从浏览器中输入URL并回车,到显示器上看到网页,这中间都发生了什么?

一、导航

导航是加载 web 页面的第一步。它发生在以下情形:用户通过在地址栏输入一个URL、点击一个链接、提交表单或者是其他的行为。

1.DNS 查找

导航的第一步是解析域名,找到页面资源的位置。

浏览器通过服务器名称请求 DNS 进行查找,最终返回一个IP地址,第一次初始化请求之后,这个IP地址可能会被缓存一段时间,这样可以通过从缓存里面检索IP地址而不是再通过域名服务器进行查找来加速后续的请求。其具体过程如下:

  1. 查找浏览器缓存:浏览器会缓存2-30分钟访问过网站的 DNS 信息
  2. 检查系统缓存:检查hosts文件,它保存了一些访问过网站的域名和IP的数据
  3. 检查路由器缓存:路由器有自己的DNS缓存
  4. 检查ISP DNS缓存:ISP服务商DNS缓存(本地服务器缓存)
  5. 递归查询:从根域名服务器到顶级域名服务器再到极限域名服务器依次搜索对应目标域名的IP

DNS 查找对于性能来说是一个问题,特别是对于移动网络。当一个用户用的是移动网络,每一个 DNS 查找必须从手机发送到信号塔,然后到达一个认证 DNS 服务器。手机、信号塔、域名服务器之间的距离可能是一个大的时间等待。

2.TCP 握手协议

一旦获取到服务器IP地址,浏览器就会通过TCP 三次握手 与服务器建立连接。这个机制的是用来让两端尝试进行通信—浏览器和服务器在发送数据之前,通过上层协议Https可以协商网络TCP套接字连接的一些参数。

  • 第一次握手:客户端向服务器端发送请求等待服务器确认
  • 第二次握手:服务器收到请求并确认,回复一个指令
  • 第三次握手:客户端收到服务器的回复指令并返回确认
3.TLS 协商

TLS 是 Transport Layer Security 的缩写,中文翻译 传输层安全性协议

为了在 HTTPS 上建立安全连接,另一种握手是必须的。更确切的说是 TLS 协商 ,它决定了什么密码将会被用来加密通信,验证服务器,在进行真实的数据传输之前建立安全连接。在发送真正的请求内容之前还需要三次往返服务器。

虽然建立安全连接对增加了加载页面的等待时间,对于建立一个安全的连接来说,以增加等待时间为代价是值得的,因为在浏览器和web服务器之间传输的数据不可以被第三方解密。

二、响应

一旦我们建立了到 web 服务器的连接,浏览器就代表用户发送一个初始的 HTTP GET 请求,对于网站来说,这个请求通常是一个 HTML 文件。 一旦服务器收到请求,它将使用相关的响应头和HTML的内容进行回复。

三、解析

解析是浏览器将通过网络接收的数据转换为 DOM树 和 CSSOM树 的步骤,通过渲染器把 DOM树 和 CSSOM树 在屏幕上绘制成页面。

1.构建DOM树

第一步是处理 HTML 标记并构造 DOM 树。HTML解析涉及到标记化和树的构造。HTML标记包括开始和结束标记,以及属性名和值。 如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建文档树。

当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个CSS文件时,解析也可以继续进行,但是对于 script 标签(特别是没有 async 或者 defer 属性)会阻塞渲染并停止HTML的解析。尽管浏览器的预加载扫描器加速了这个过程,但过多的脚本仍然是一个重要的瓶颈。

2.构建CSSOM树

第二步是处理 CSS 并构建 CSSOM 树。CSS 对象模型和 DOM 是相似的。DOM 和 CSSOM是两棵树。它们是独立的数据结构。浏览器将CSS 规则转换为可以理解和使用的样式映射。浏览器遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树。

JavaScript 编译

当 CSS 被解析并创建 CSSOM 时,其他资源,包括JavaScript文件正在下载(多亏了preload scanner)。JavaScript被解释、编译、解析和执行。脚本被解析为抽象语法树。一些浏览器引擎使用 Abstract Syntax Tree 并将其传递到解释器中,输出在主线程上执行的字节码。这就是所谓的 JavaScript 编译。

构建辅助功能树

浏览器还构建辅助设备用于分析和解释内容的辅助功能树。可访问性对象模型(AOM)类似于DOM的语义版本。当DOM更新时,浏览器会更新辅助功能树。辅助技术本身无法修改可访问性树。

四、渲染

渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的 CSSOM 树和 DOM 树组合成一个 Render 树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在 GPU 而不是CPU 上绘制屏幕的一部分来提高性能,从而释放主线程。

1.样式计算

第三步是将 DOM 树和 CSSOM 树组合成一个 Render 树,计算样式树或渲染树从 DOM 树的根开始构建,遍历每个可见节点。

2.布局计算

第四步是在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。

第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。

3.绘制

最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为首次有效绘制(first meaningful paint)。在绘制阶段,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素。

合成

绘制可以将布局树中的元素分解为多个层(类似层级上下文)。当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。

层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。

解析 html 生成 DOM 树,解析 css,生成 CSSOM 树,将 DOM 树和 CSSOM 树结合,生成渲染树;

根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系 - 回流;

根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置 - 重绘;

最后将得到的节点位置信息交给浏览器的图形处理程序,让浏览器中显示页面。

五、交互

主线程绘制页面完成后,页面不一定是可用的。比如:有 JS 文件可能是2 MB,而且用户的网络连接很慢。在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。

重流和重绘

渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。

页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。

重流

当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为重流(回流)。 会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的 DOM 元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()

一些会导致回流的CSS属性:

width, height, padding, border, margin, position, top, left, bottom, right, float, clear, text-align, vertical-align, line-height, font-weight, font-size, font-family, overflow, white-space

重绘 (Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

容易造成重绘操作的css:

color, border-style, border-radius, text-decoration, box-shadow, outline, background

如何进行优化,减少重绘和重流

浏览器自己的优化

每次回流都会对浏览器造成额外的计算消耗,所以浏览器对于回流和重绘有一定的优化机制。浏览器通常都会将多次回流操作放入一个队列中,等过了一段时间或操作达到了一定的临界值,然后才会挨个执行,这样能节省一些计算消耗。但是在获取布局信息操作的时候,会强制将队列清空,也就是强制回流,比如访问或操作以下或方法时:

clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()

这些属性或方法都需要得到最新的布局信息,所以浏览器必须去回流执行。因此,在项目中,尽量避免使用上述属性或方法,如果非要使用的时候,也尽量将值缓存起来,而不是一直获取。

减少重流与重绘

CSS:

  • 避免使用 table|flex 布局。可能很小的一个小改动会造成整个 table|flex 的重新布局。
  • 使用 visibility 替换 display: none 。因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 尽可能在 DOM 树的最末端改变 class。回流是不可避免的,但可以减少其影响,可以限制回流的范围,使其影响尽可能少的节点。
  • 避免设置多层内联样式;
  • 将动画效果应用到 position 属性为 absolute|fixed 的元素上。
  • 避免使用CSS表达式

  • 使用 transform 替代 top

  • 将频繁重绘或者回流的节点设置为图层。图层能够阻止该节点的渲染行为影响别的节点。如: will-change、video、iframe 等标签,浏览器会自动将该节点变为图层。
  • css3硬件加速(GPU加速)。 使用css3硬件加速,可以让 transform、opacity、filters、will-change 这些动画不会引起回流重绘 。但对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

JavaScript:

  • 避免频繁操作样式。最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
  • 避免频繁操作DOM。创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。也可以先为元素设置 display: none ,操作结束后再把它显示出来。因为在 display: none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性。如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。

浏览器加载JavaScript脚本

JavaScript 是一种具有函数优先的轻量级、解释型或即时编译型的编程语言。虽然它是作为开发 Web 页面的脚本语言而出名的,但是它也被用到了很多非浏览器环境中,例如 Node.js、 Apache CouchDB 和 Adobe Acrobat。JavaScript 是一种基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

JavaScript 的标准是 ECMAScript 。截至 2012 年,所有的现代浏览器都完整的支持 ECMAScript 5.1,旧版本的浏览器至少支持 ECMAScript 3 标准。2015 年 6 月 17 日,ECMA 国际组织发布了 ECMAScript 的第六版,该版本正式名称为 ECMAScript 2015,但通常被称为 ECMAScript 6 或者 ES6。自此,ECMAScript 每年发布一次新标准。

嵌入 JavaScript 代码

在 HTML 文档里嵌入 JavaScript 代码有四种方法:

  • 在 HTML 标签的事件属性中直接添加脚本代码。

    <input type="button" onClick="Javascript:console.log(this.value);" value="Hello, Lizhao!"/>
    
  • 使用 script 标签在网页中直接插入脚本代码。

    <script type="text/javascript">
        console.log("JavaScript!")
    </script>
    

    script 标签有一个 type 属性,用来指定脚本类型。属性的值为 MIME 类型,支持的 MIME 类型包括:text/javascript(默认值)、text/ecmascript、application/javascript、application/ecmascript。如果 MIME 类型不是 JavaScript 类型,则该元素所包含的内容会被当作数据块而不会被浏览器执行。

    如果 type 属性为module,代码会被当作 JavaScript 模块。

  • 使用 script 标签链接外部脚本文件。

    <script type="text/javascript" src="xxx.js"></script>
    
  • 在 URL 使用特殊的 Javascript: 协议。

    <a href="javascript:new Date().toLocaleTimeString();">
        现在是什么时间了?
    </a>
    

JavaScript 加载流程

浏览器加载 JavaScript 脚本,其正常流程如下:

  • 浏览器的渲染引擎持有渲染的控制权,它正常解析 HTML 页面。
  • 解析遇到 script 标签,渲染引擎移交控制权给 Javascript 引擎。
  • 如果 script 标签引用了外部脚本那就先下载再执行,否则直接执行代码。
  • JavaScript 引擎执行完毕移交控制权给渲染引擎,渲染引擎继续解析。

浏览器可以同时并行加载多个 .js 文件,但下载后并不一定立即执行,而是按引入的顺序执行,也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。

此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。

defer属性

浏览器解析到包含 defer 属性的 script 元素时,其运行流程如下:

  • 浏览器的呈现引擎持有渲染的控制权,它正常解析 HTML 页面。

  • 解析遇到包含 defer 属性的 script 标签,继续解析 HTML,同时并行下载外链脚本。

  • 解析完成,文档处于交互状态时开始解析处于 deferred 模式的脚本。

  • 脚本解析完毕后,将文档状态设置为完成,DOMContentLoaded 事件随之触发。

使用 defer 属性时需要注意的点:

  • defer 属性下载的脚本文件在 DOMContentLoaded 事件触发前执行,即,刚刚读取完 html 标签。

  • defer 属性可以保证执行顺序就是它们在页面上出现的顺序。

  • 对于内置而不是加载外部脚本的 script 标签,以及动态生成的 script 标签,defer 属性不起作用。
  • 使用 defer 加载的外部脚本不应该使用 document.write 方法。

async属性

浏览器解析到包含 async 属性的 script 元素时,其运行流程如下:

  • 浏览器的呈现引擎持有渲染的控制权,它正常解析 HTML 页面。

  • 解析遇到包含 async 属性的 script 标签,继续解析 HTML,让另一进程同时并行下载外链脚本。

  • 脚本下载完成,浏览器暂停解析 HTML,开始执行下载的脚本。

  • 脚本执行完毕,浏览器恢复解析 HTML。

使用 async 属性时需要注意的点:

  • async 属性可以保证脚本下载的同时,浏览器继续渲染。
  • async 属性无法保证脚本的执行顺序,哪个先下载结束就先执行哪一个。
  • 包含 async 属性的脚本不应该使用 document.write 方法。
  • 如果同时使用 async 和 defer 属性,后者不起作用,浏览器行为由 async 属性决定。

脚本的动态加载

JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。

script 元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。动态生成的 script 标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。如果想避免这个问题,可以设置 async 属性为 false。还可以监听脚本的 onload 事件来为脚本指定回调。

CSS 阻塞 JS 加载

因为 JS 脚本可能会引用 DOM 的样式做计算,所以为了保证脚本计算的正确性,Firefox 浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit 则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。

预加载扫描器

当主线程在解析 HTML 和 CSS 时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当 JavaScript 解析和执行顺序不重要时,可以添加 async 属性或 defer 属性。

请注意,预加载扫描器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

浏览器引擎前缀

浏览器厂商们有时会给实验性的或者非标准的 CSS 属性和 JavaScript API 添加前缀,这样开发者就可以用这些新的特性进行试验,同时(理论上)防止他们的试验代码被依赖,从而在标准化过程中破坏 web 开发者的代码。开发者应该等到浏览器行为标准化之后再使用未加前缀的属性。

CSS 前缀

主流浏览器引擎前缀:

  • -webkit-:Chrome、Safari、新版Opera浏览器,以及几乎所有 iOS 系统中的浏览器(包括 iOS 系统中的火狐浏览器);基本上所有基于 WebKit 内核的浏览器
  • -moz-:firefox 浏览器
  • -o-:旧版 Opera 浏览器
  • -ms-:IE、Edge 浏览器
-webkit-transition: all 4s ease;
-moz-transition: all 4s ease;
-ms-transition: all 4s ease;
-o-transition: all 4s ease;
transition: all 4s ease;

API 前缀

过去,浏览器引擎也使用前缀修饰实验性质的 API。如果整个接口都是实验性的,前缀修饰的就是接口名(但不包括其中的属性或者方法)。如果将一个实验性的接口或者方法添加到一个标准化的接口中,这个新增的接口或者方法被前缀修饰。

接口前缀,需要使用大写的前缀修饰接口名:

  • Webkit:Chrome、Safari、新版Opera浏览器,以及几乎所有 iOS 系统中的浏览器(包括 iOS 系统中的火狐浏览器);基本上所有基于 WebKit 内核的浏览器
  • Moz:firefox 浏览器
  • O:旧版 Opera 浏览器
  • Ms:IE、Edge 浏览器

属性和方法前缀,需要使用小写的前缀修饰属性或者方法:

  • webkit:Chrome、Safari、新版Opera浏览器,以及几乎所有 iOS 系统中的浏览器(包括 iOS 系统中的火狐浏览器);基本上所有基于 WebKit 内核的浏览器

  • moz:firefox 浏览器

  • o:旧版 Opera 浏览器

  • ms:IE、Edge 浏览器

    var requestAnimationFrame = window.requestAnimationFrame || 
        window.mozRequestAnimationFrame || 
        window.webkitRequestAnimationFrame || 
        window.oRequestAnimationFrame || 
        window.msRequestAnimationFrame;
    

相关问题

渲染页面时常见哪些不良现象?

  • FOUC:无样式内容闪烁(Flash Of Unstyled Content),主要指的是样式闪烁的问题,由于浏览器渲染机制(比如:firefox),在 CSS 加载之前,先呈现了 HTML,就会导致展示出无样式内容,然后样式突然呈现的现象。会出现这个问题的原因主要是 css 加载时间过长,或者 css 被放在了文档底部。

  • 白屏:有些浏览器渲染机制(比如:chrome)要先构建 DOM 树和 CSSOM 树,构建完成后再进行渲染,如果 CSS 部分放在 HTML 尾部,由于 CSS 未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把 js 文件放在头部,脚本的加载会阻塞后面文档内容的解析,从而页面迟迟未渲染出来,出现白屏问题。

什么是 javascript: 协议?

javascript: 协议是在 URL 前面跟一个 javascript: 协议限定符,是另一种嵌入 JavaScript 代码到客户端的方式。这种特殊的协议类型指定 URL 内容为任意字符串,这个字符串是会被 JavaScript 解释器运行的 JavaScript 代码。javascript:URL 能识别的「资源」是转换成字符串的执行代码的返回值,如果代码返回 undefined,那么这个资源是没有内容的。

javascript:URL 可以用在任意使用常规 URL 的地方,比如 a 标签的 href 属性,form 标签的 action 属性,甚至 window.open() 方法的参数。

<a href="javascript:new Date().toLocaleTimeString();">
    现在是什么时间了?
</a>

部分浏览器(Firefox、Chrome)会执行 URL 里面的代码,并使用返回的字符串作为待显示新文档的内容。但是有些浏览器(如 Safari)则不支持这种语法,但是类似这样的 URL 还是支持的:

<a href="javascript:alert(new Date().toLocaleTimeString());">
    现在是什么时间了?
</a>

上面两种方式:第一种会使用返回值覆盖当前文档,第二种则不会。如果要确保不覆盖当前文档,可以用 void 操作符强制函数调用或给表达式赋予 undefined 值。

<a href="javascript:void window.open('about:blank');">打开一个窗口</a>

javascript:URL 还可以用于测试一小段 JavaScript 代码,在浏览器地址栏直接输入 javascript:URL 即可。

此外,还可以用于浏览器书签,在 Web 浏览器中,「书签」就是一个保存起来的 URL。如果书签是 javascript:URL 那么保存的就是一小段脚本,叫做 bookmarklet,bookmarklet 是一个小型程序,很容易就可以从浏览器的菜单或工具栏里启动,bookmarklet 里的代码执行起来就像页面上的脚本一样,可以查询和设置文档的内容、呈现和行为,只要书签不返回值,就可以操作当前显示的任何文档,而不把文档替换成新内容。

参考链接

小文:浅谈浏览器发展简史

五大主流浏览器及四大内核

MDN 浏览器引擎前缀

MDN 渲染页面:浏览器的工作原理

浏览器的工作原理:新式网络浏览器幕后揭秘

© lizhao all right reserved,powered by Gitbook文件修订时间: 2022-05-31 20:58:34

results matching ""

    No results matching ""