Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从你在浏览器输入一个网址URL... #17

Open
libin1991 opened this issue Jan 28, 2018 · 0 comments
Open

从你在浏览器输入一个网址URL... #17

libin1991 opened this issue Jan 28, 2018 · 0 comments

Comments

@libin1991
Copy link
Owner

我们在面试的时候或许经常会被问到:

从你在浏览器输入一个网址到网页内容完全被展示的这段时间内,都发生了什么事情?

确实是个老生常谈的问题,但问题的答案并不是唯一的,或许在三五年前,这个问题还会有一个「相对」标准的答案。

  1. 浏览器在接收到这个请求时,会开启一个单独的线程来处理这个请求,首先要判断用户输入是否为合法或合理的 URL 地址,是否为 HTTP 协议请求,如果是那就进入下一步
  2. 浏览器的浏览器引擎将对此 URL 进行分析加载
  3. 通过 DNS 解析域名获取该网站地址对应的 IP 地址,查询完成后连同浏览器的 Cookie、 userAgent 等信息向网站目的 IP 发出 GET 请求。
  4. 接下来就是经典的「三次握手」,HTTP 协议会话,浏览器客户端向 Web 服务器发送报文,进行通讯和数据传输。
  5. 进入网站的后端服务,如 Tomcat、Apache 等,还有近几年流行的 Node.js 服务器,这些服务器上部署着应用代码,语言有很多,如 Java、 PHP、 C++、 C# 和 Javascript 等。
  6. 服务器根据 URL 执行相应的后端应用逻辑,整理数据组装成一个完整的 HTML 数据返回给浏览器,期间会使用到「服务器缓存」或「数据库」内的内容。
  7. 浏览器接收到返回信息后先判读此 HTML 文件是否存在本地缓存,如果不存在或不可用,则下载此 HTML 文件(200状态码),如果可用(未过期),则走浏览器缓存(304返回码)。「强缓存(200返回码)不在考虑范围」
  8. 浏览器的渲染引擎在拿到 HTML 文件后,便开始解析构建 DOM 数,并根据 HTML 中的标记请求下载指定的 MIME 类型文件(如 CSS、 JavaScript 脚本等),同时使用&设置缓存等内容。
  9. 渲染引擎根据 CSS 样式规则将 DOM 树扩充为渲染树,然后进行重排、重绘。
  10. 如果含有 JS 文件将会执行,进行 Dom 操作、缓存读存、时间绑定等操作。最终页面将被展示在浏览器上。

此答案精简的概括了「后端为主的 MVC 模式」及早期 Web 应用的浏览器相应的全过程。那,前端技术发展到现在,「前后端分离」「中间件直出」和「MNV*模式」也已问世,再谈及此问题,答案会有不同。

就以「前后端分离」为例,在上方答案的第4步后,紧接着就不会直接进入后端服务器了。而会被 HTTP 和反向代理服务器,如 Ngnix,代替。

  • 前置步骤1、2、3、4
  • Ngnix 在接收到 HTTP(80端口)或 HTTPS(443端口)后,根据 URL 做服务器分发,分发(rewrite)到后端服务器或静态资源服务器,首页请求基本是静态服务器,返回一个静态的 HTML 文件
  • 步骤7、8、9
  • 执行 JS 脚本,异步 ajax、 fetch 发起 POST、 GET 请求,重新进入 Ngnix 分发,此次分发到后端服务器,步骤5、6,然后返回一个 xml 或 json 格式的信息,一般含有 code(返回码)、result(依赖信息)
  • 最后根据返回码执行不同的 js 逻辑,增删改页面元素,此时可能会发生重排或重汇。首页加载结束。

以上步骤可以发现,浏览器可能会触发重绘两次,极易发生「白屏」或「页面抖动」,为了解决这个问题「中间件直出」的模式应运而生。另外为了扩充大前端的阵营,吸纳 IOS 和 Android,Google又设计了「MNV*模式」,典型代表就是 ReactNative,但此模式已经脱离了浏览器的范畴,此处就不再做扩展。

以上讨论的渲染过程中使用到了较多的浏览器功能,如用户地址栏输入框、网络请求、浏览器文档解析、渲染引擎渲染网页、 JavaScript 引擎执行js脚本、客户端存储等。 接下来我们介绍下浏览器的基本结构组成。

浏览器的结构组成

浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储) 如下图:

  • 用户界面-包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了页面显示窗口之外的其他部分
  • 浏览器引擎-可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心
  • 渲染引擎-解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,也有人称之为排版引擎,我们常说的浏览器内核主要指的就是渲染引擎
  • 网络-用来完成网络调用或资源下载的模块
  • UI 后端-用来绘制基本的浏览器窗口内控件,如输入框、按钮、单选按钮等,根据浏览器不同绘制的效果也不同
  • JS解释器-用来解释执行JS脚本的模块,如 V8 引擎
  • 数据存储-浏览器在硬盘中保存 cookie、localStorage等各种数据,可通过浏览器引擎提供的API进行调用

作为前端开发人员,我们需要重点理解渲染引擎的工作原理,灵活应用数据存储技术,在实际项目开发中会经常涉及到这两个部分,尤其是在做项目性能优化时,理解浏览器渲染引擎尤为重要。而其他部分则是由各种浏览器自行管理的,开发者能控制的地方较少。今天我们就围绕这两个重点其中的一个部分「浏览器渲染引擎」进行展开

浏览器渲染引擎

浏览器渲染引擎是由各大浏览器厂商依照 W3C 标准自行实现的,也被称之为「浏览器内核」。

目前,市面上使用的主流浏览器内核有5类:Trident、Gecko、Presto、Webkit、Blink。

Trident:俗称 IE 内核,也被叫做 MSHTML 引擎,目前在使用的浏览器为 IE11-,以及各种国产多核浏览器中的IE兼容模式。另外Edge 浏览器不再使用 MSHTML 引擎,而是使用类全新的引擎 EdgeHTML。

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

Presto:Opera 前内核,为啥说是前内核呢?因为 Opera12.17 以后便拥抱了 Google Chrome 的 Blink 内核,此内核就没了寄托

Webkit:Safari 内核也是 Chrome 内核原型,主要是 Safari 浏览器在使用的内核,也是特性上表现较好的浏览器内核。也被大量使用在移动端浏览器上。

Blink: 由Google和Opera Software开发的浏览器排版引擎,在Chrome(28及往后版本)、Opera(15及往后版本)和Yandex浏览器 中使用。Blink 其实是 WebKit 的一个分支,添加了一些优化的新特性,例如跨进程的 iframe,将 DOM 移入 JavaScript 中来提高 JavaScript 对 DOM 的访问速度等,目前较多的移动端应用内嵌的浏览器内核也渐渐开始采用 Blink。

渲染引擎的工作流程

浏览器渲染引擎重要的工作就是将 HTML 和 CSS 文档解析组合最终渲染到浏览器窗口上。如下图所示,渲染引擎在接受到 HTML 文件后主要进行了以下操作:解析 HTML 构建 DOM 树 -> 构建渲染树 -> 渲染树布局 -> 渲染树绘制。

解析 HTML 构建 DOM 树时渲染引擎会将 HTML 文件的便签元素解析成多个 DOM 元素对象节点,并且将这些对象根据父子关系组成一个树结构。同时 CSS 文件被解析成 CSS规则表,然后将每条 CSS 规则按照「从右向左」的方式在 DOM 树上进行逆向匹配,生成一个具有样式规则描述的 DOM 渲染树。接下来就是将渲染树进行布局、绘制的过程。首先根据 DOM 渲染树上的样式规则,对 DOM 元素进行大小和位置的定位,关键属性如position;width;margin;padding;top;border;...,接下来在根据元素样式规则中的color;background;shadow;...规则进行 DOM 的绘制。

另外,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

再者,需要注意的是,在浏览器渲染完首屏页面后,如果对 DOM 进行操作会引起浏览器引擎对 DOM 渲染树的重新布局和重新绘制,我们叫做「重排」和「重绘」,由于重排和重绘是前后依赖的关系,所以重绘发生时未必会触发渲染引擎的重排,但是如果发生了重排就必然会触发重绘操作,这样带来的性能损害就是巨大的。因此我们在做性能优化的时候应该遵循「避免重排;减少重绘」的原则。

不同浏览器内核间的差异

在不同的浏览器内核下, 浏览器页面渲染的流程略有不同

上面两幅图分别是 Webkit 和 Geoko 内核渲染 DOM 的工作流程,对比可以看出,两者流程的区别主要在于 CSS 样式表的解析时机,Webkit 内核下,HTML 和 CSS 文件的解析是同步的,而 Geoko 内核下,CSS 文件需要等到 HTML 文件解析成内容 Sink 后才进行解析。

另外两者的不同还有描述术语,除此之外两者的流程就基本相同了,其中最重要的三个部分就是 「HTML 的解析」「CSS 的解析」「渲染树的生成」。这三个部分的原理内容就比较深,涉及到「词法分析」「语法分析」「转换」「解释」等数据结构的内容,比较枯燥,一般我们了解到这里就够了,不过想深入了解的同学可以阅读此篇译文,浏览器的工作原理,里面详细的解释了以上三个部分的流程和关系。此处就不再多做赘述了。

关于 CSS 规则的匹配

上面我们提到过, CSS 规则是按照「从右向左」的方式在 DOM 树上进行逆向匹配的,最终生成一个具有样式规则描述的 DOM 渲染树。

但是你知道为什么要「从右向左」做逆向匹配码?

我们重新回到【webkit 内核工作流程】图

CSS 规则匹配是发生在webkit引擎的「Attachment」过程中,浏览器要为每个 DOM Tree 中的元素扩充 CSS 样式规则(匹配 Style Rules)。对于每个 DOM 元素,必须在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。

我们来举一个最简单的栗子:

<template>
<div>
  <div class="t">
    <span>test</span>
    <p>test</p>
  <div>
</div>
</template>

<style>
div{ color: #000; }
div .t span{ color: red; }
div .t p{color: blue; }
</style>

此处我们有一个 html 元素 和一个 style 元素,两者需要做遍历匹配

此处会有 4*3 个匹配项,如果做正向匹配,在遇到 <span> 标签匹配 div .t p{ color: red; }到匹配项时,显然时不通过到,计算机首先要找到<span> 标签到父标签和祖父标签,判断他们是否满足div .t的规则,然后在匹配<span>是否为p标签,此处匹配不成功,此处就产生了三次浪费。

如果时逆向匹配,那么第一次对比<span>是否为p标签便可排除此规则,效率更高。

如果将 HTML 结构变复杂,CSS 规则表变庞大,那么,「逆向匹配」的优势就远大于「正向匹配」了,因为匹配的情况远远低于不匹配的情况。同时如果在选择器结尾加上通配符「*」,那么「逆向匹配」的优势就大打折扣,这也就是很多优化原则提到的尽量避免在选择器末尾添加通配符的原因。

极限了想,如果我们的样式表不存在嵌套关系,如下:

<template>
<div>
  <div>
    <span class="div_t_span">test</span>
    <p class="div_t_p">test</p>
  <div>
</div>
</template>

<style>
div{ color: #000; }
.div_t_span{ color: red; }
.div_t_p{color: blue; }
</style

那么引擎的「Attachment」过程将得到极大的精简,效率也是可想而知的,这就是为什么「微信小程序」样式表不建议使用关系行写法的原因。

相关的性能优化

由以上介绍,我们大致可以在案例中看到同浏览器渲染引擎相关的可行优化点。

大致为以下几种

  1. 减少 JS 加载对 Dom 渲染的影响:将 JS 文件放在 HTML 文档后加载,或者使用异步的方式加载 JS 代码
  2. 避免重排,减少重绘:在做css动画的时候减少使用 width、 margin、 padding等影响 CSS 布局对规则,可以使用 CSS3 的 transform 代替
  3. 减少使用关系型样式表的写法:直接使用唯一的类名即可最大限度的提升渲染效率
  4. 减少 DOM 的层级:减少无意义的dom 层级可以减少 渲染引擎 Attachment 过程中的匹配计算量
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant