【前端系列教程之JavaScript】13_JavaScript浏览器深入解析

本文详细剖析浏览器的组成结构,涵盖用户界面、浏览器引擎、渲染引擎、网络功能、JavaScript引擎等,讲解了HTML/CSS解析、渲染过程、回流重绘优化以及缓存机制,助你理解浏览器工作原理和性能提升技巧。
摘要由CSDN通过智能技术生成

浏览器深入解析

浏览器的组成

        浏览器在不断的演变中,并没有被要求呈现出一种特定的形态,但基本包括了如用户地址栏输入框、网络请求、浏览器文档解析、渲染引擎渲染网页、 JavaScript 引擎执行 js 脚本、客户端存储等功能。

        从原理构成上分为七个模块,分别是User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI后端)、Date Persistence(数据持久化存储)。

结构如下所示:

        和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个渲染引擎实例。每个标签页都是一个独立的进程。

        其中,最重要的是渲染引擎(内核)和JavaScript解释器(JavaScript引擎)。浏览器内核主要负责 HTML、CSS 的解析,页面布局、渲染与复合层合成,主流的内核有:Blink、Webkit、Gecko、EdgeHTML、Trident。

        JavaScript引擎负责 JavaScript 代码的解释与执行,主流的 JavaScript 引擎有:V8、SpiderMonkey、JavaScriptCore、Chakra。

        首先浏览器是一个软件,就和qq,wechat,没什么差别,只是功能不一样。qq,wechat是社交类,而浏览器就是专门用来访问和浏览万维网页面的客户端软件。

1. User Interface(用户界面):

        包括工具栏、地址栏、前进/后退按钮、书签菜单、可视化页面加载进度、智能下载处理、首选项、打印等。除了浏览器主窗口显示请求的页面之外,其他显示的部分都属于用户界面。

2. Browser engine(浏览器引擎):

        浏览器引擎是一个可嵌入的组件,其为渲染引擎提供高级接口。

        浏览器引擎可以加载一个给定的URI,并支持诸如:前进/后退/重新加载等浏览操作。

        浏览器引擎提供查看浏览会话的各个方面的挂钩,例如:当前页面加载进度。

        浏览器引擎还允许查询/修改渲染引擎设置。

3. Rendering Engine(渲染引擎):

        渲染引擎为指定的URI生成可视化的表示。

        渲染引擎能够显示HTML和XML文档,可选择CSS样式,以及嵌入式内容(如图片)。

        渲染引擎能够准确计算页面布局,可使用“回流”算法逐步调整页面元素的位置。

        渲染引擎内部包含HTML解析器。

        Chrome为每个Tab分配了各自的渲染引擎实例,每个Tab就是一个独立的进程。

4. Networking(网络)

        网络系统实现HTTP和FTP等文件传输协议。

        网络系统可以在不同的字符集之间进行转换,为文件解析MIME媒体类型。

        网络系统可以实现最近检索资源的缓存功能。

5. JavaScript Interpreter(JS解释器)

        JavaScript解释器能够解释并执行嵌入在网页中的JavaScript(又称ECMAScript)代码。 为了安全起见,浏览器引擎或渲染引擎可能会禁用某些JavaScript功能,如弹出窗口的打开。

6. XML Parser(XML解析器)

        XML解析器可以将XML文档解析成文档对象模型(Document Object Model,DOM)树。 XML解析器是浏览器架构中复用最多的子系统之一,几乎所有的浏览器实现都利用现有的XML解析器,而不是从头开始创建自己的XML解析器

        功能相似的HTML解析器和XML解析器为什么前者划分在渲染引擎中,后者作为独立的系统?

        XML解析器对于系统来说,其功能并不是关键性的,但是从复用角度来说,XML解析器是一个通用的,可重用的组件,具有标准的,定义明确的接口。相比之下,HTML解析器通常与渲染引擎紧耦合。

7. UI Backend(显示后端)

        用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。

8. Data Persistence(数据持久层)

        数据持久层将与浏览会话相关联的各种数据存储在硬盘上。 这些数据可能是诸如:书签、工具栏设置等这样的高级数据,也可能是诸如:Cookie,安全证书、缓存等这样的低级数据。

浏览器内核

        浏览器内核负责对网页语法的解释(如标准通用标记语言下的一个应用HTML、JavaScript)并渲染(显示)网页。

        浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。

        内核分为两个部分:渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎。

        不同的浏览器内核对网页编写语法的解释也有不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,这也是网页编写者需要在不同内核的浏览器中测试网页显示效果的原因。

1. 渲染引擎

        负责请求网络页面资源加以解析排版并呈现给用户,渲染引擎可以显示html、xml文档及图片,它也可以借助插件显示其他类型数据,例如使用PDF阅读器插件,可以显示PDF格式。

2. js引擎

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

3. 当前主流内核

Trident ([‘traɪd(ə)nt]):普遍称作 “IE内核”

        该内核程序在 1997 年的 IE4 中首次被采用,是微软在 Mosaic(”马赛克”,这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览) 代码的基础之上修改而来的,并沿用到 IE11,也被普遍称作 “IE内核”。

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

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

Gecko ([‘gekəʊ]):Firefox 内核

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

Webkit:

        webkit内核 可以说是以硬件盈利为主的苹果公司给软件行业的最大贡献之一。随后,2008 年谷歌公司发布 chrome 浏览器,采用的 chromium 内核便 fork 了 Webkit。

Chromium/Blink:

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

        chromium fork 自开源引擎 webkit,却把 WebKit 的代码梳理得可读性提高很多,所以以前可能需要一天进行编译的代码,现在只要两个小时就能搞定。因此 Chromium 引擎和其它基于 WebKit 的引擎所渲染页面的效果也是有出入的。所以有些地方会把 chromium 引擎和 webkit 区分开来单独介绍,而有的文章把 chromium 归入 webkit 引擎中,都是有一定道理的。

        谷歌公司还研发了自己的 Javascript 引擎,V8,极大地提高了 Javascript 的运算速度。

        Blink 引擎问世后,国产各种 chrome 系的浏览器也纷纷投入 Blink 的怀抱。

        Blink是一个由Google和Opera Software开发的浏览器排版引擎,Opera表示将会跟随谷歌采用其Blink浏览器核心,同时参与了Blink的开发。

Presto ([‘prestəʊ]):opera 的 “前任” 内核

        为何说是 “前任”,因为最新的 opera 浏览器早已将之抛弃从而投入到了谷歌大本营。

JavaScript引擎

        JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。

Mozilla

● SpiderMonkey,第一款JavaScript引擎,由Brendan Eich在Netscape Communications时编写,用于Mozilla Firefox 1.0~3.0版本。

● Rhino,由Mozilla基金会管理,开放源代码,完全以Java编写。

● TraceMonkey,基于实时编译的引擎,其中部份代码取自Tamarin引擎,用于Mozilla Firefox 3.5~3.6版本。

● JaegerMonkey,结合追踪和组合码技术大幅提高性能,部分技术借凿了V8、JavaScriptCore、WebKit,用于Mozilla Firefox 4.0以上版本。

Google

● V8,开放源代码,由Google丹麦开发,是Chrome浏览器的一部分。

        V8是被设计用来提高网页浏览器内部JavaScript执行的性能,那么如何提高性能呢?

        为了提高性能,v8会把js代码转换为高效的机器码,而不在是依赖于解释器去执行。v8引入了JIT在运行时把js代码进行转换为机器码。这里的主要区别在于V8不生成字节码或任何中间代码。

        V8介绍:JavaScript V8引擎 - 简书

        JIT:JIT 为什么能大幅度提升性能? - 知乎

微软

● Chakra (JScript引擎),中文译名为查克拉,用于Internet Explorer 9的32位版本。

Opera

● Linear A,用于Opera 4.0~6.1版本。

● Linear B,用于Opera 7.0~9.2版本。

● Futhark,用于Opera 9.5~10.2版本。

● Carakan,由Opera软件公司编写,自Opera10.50版本开始使用。

其它

● KJS,KDE的ECMAScript/JavaScript引擎,最初由Harri Porten开发,用于KDE项目的Konqueror网页浏览器中。

● Narcissus,开放源代码,由Brendan Eich编写(他也参与编写了第一个SpiderMonkey)。

● Tamarin,由Adobe Labs编写,Flash Player 9所使用的引擎。

● Nitro(原名SquirrelFish),为Safari 4编写。

浏览器的渲染过程

渲染过程

从上面这个图上,我们可以看到,浏览器渲染过程如下:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树

  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)

  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)

  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素

  5. Display:将像素发送给GPU,展示在页面上

生成渲染树

        渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。

  • 为了构建渲染树,浏览器主要完成了以下工作:

    • 从DOM树的根节点开始遍历每个可见节点。

    • 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。

    • 根据每个可见节点以及其对应的样式,组合生成渲染树

  • 什么是不可见节点

    • 一些不会渲染输出的节点,比如script、meta、link等。

    • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的(因为还占据文档空间)。只有display:none的节点才不会显示在渲染树上。

  • 注意:渲染树只包含可见的节点

回流和重绘

回流

        前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

        为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

        我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值(如下图)

重绘

        最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

分辨率和像素是什么关系? - 知乎实际像素值和屏幕像素:分辨率和像素是什么关系? - 知乎

何时发生回流重绘

        既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

        我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素

  • 元素的位置发生变化

  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。

  • 页面一开始渲染的时候(这肯定避免不了)

  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

        注意:回流一定会触发重绘,而重绘不一定会回流.

        根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。

减少回流和重绘

减少回流和重绘(一):最小化重绘和重排

        由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉

考虑这个例子:

var el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px'

        例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。

        因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:

  • 使用cssText

var el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px;   padding: 5px;
  • 修改CSS的class
var el = document.getElementById('test');
// 在使用 += 操作class的时候,一定要记得添加一个空格
el.className += ' active';

减少回流和重绘(二):批量修改DOM

        当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流,不是指的定位脱离文档流

  2. 对其进行多次修改

  3. 将元素带回到文档中。

        该过程的第二步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

        有三种方式可以让DOM脱离文档流:

  1. 隐藏元素,应用修改,重新显示

  2. 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。

  3. 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

        考虑我们要执行一段批量插入节点的代码:

function appendDataToElement(appendToElement, data) {
    var li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

var ul = document.getElementById('list');
appendDataToElement(ul, data);

        如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

        我们可以使用这三种方式进行优化:

        方式1: 隐藏元素,应用修改,重新显示

        这个会在展示和隐藏节点的时候,产生两次重绘

function appendDataToElement(appendToElement, data) {
    var li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
var ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

        方式2:使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

        createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。

var ul = document.getElementById('list');

var fragment = document.createDocumentFragment();

appendDataToElement(fragment, data);

ul.appendChild(fragment);

方式3:将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

var ul = document.getElementById('list');

var clone = ul.cloneNode(true);

appendDataToElement(clone, data);

ul.parentNode.replaceChild(clone, ul);

减少回流和重绘(三):对于复杂动画效果,使用绝对定位让其脱离文档流

        对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。

减少回流和重绘(四):css3硬件加速(GPU加速)

        比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

        划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

        常见的触发硬件加速的css属性:

  • transform

  • opacity

  • filters

  • Will-change

        如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。

浏览器静态资源缓存机制

为什么需要缓存?

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

        对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求(强缓存),或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据(协商缓存)。

缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

  • Service Worker

  • Memory Cache

  • Disk Cache

  • Push Cache

1.Service Worker

        Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

        Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

        当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

2.Memory Cache

        Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

        那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

        这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

        当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

        内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

        需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

3.Disk Cache

        Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储持久性上。

        在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。

        浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

        关于这点,网上说法不一,不过以下观点比较靠得住:

        对于大文件来说,大概率是不存储在内存中的,反之优先

        当前系统内存使用率高的话,文件优先存储进硬盘

4.Push Cache

        Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

        Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake Archibald的 HTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差

  • 可以推送 no-cache 和 no-store 的资源

  • 一旦连接被关闭,Push Cache 就被释放

  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。

  • Push Cache 中的缓存只能被使用一次

  • 浏览器可以拒绝接受已经存在的资源推送

  • 你可以给其他域名推送资源

    如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

        那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值