作为前端开发,我们的日常工作中除了写代码之外,几乎大多数的时间都在跟浏览器打交道。当然,现在我们甚至写代码都可以直接在浏览器里完成,一个浏览器走天下。
因此,我们应该对浏览器的了解要更加深入,除了了解怎么使用和调试浏览器,我们还要掌握它是怎样将我们编写的代码渲染到页面中的。
浏览器的主要功能,是通过向服务器请求并在浏览器窗口中展示 Web 资源内容,通常包括 HTML 文档、PDF、图片等,我们也可以通过插件的方式加载更多其他的资源类型(比如播放视频)。
对于浏览器的问题,HTTP 请求相关的,想必各位在面试的时候都被问烂了吧,这里直接过一下浏览器中的 HTTP 请求过程:
这篇文章会重点介绍第 6 步,该步骤涉及浏览器的渲染过程和原理。除了初次加载页面,用户的很多操作都同样涉及到浏览器渲染,比如以下功能:
除了这些,实际上我们和浏览器的几乎所有操作,都涉及到浏览器的渲染过程。为了更深刻地认识这些过程,我们先来认识下浏览器的结构。
HTML 和 CSS 规范中规定了浏览器解析和渲染 HTML 文档的方式,曾经各个浏览器都只遵循其中一部分,因此前端开发经常需要兼容各种浏览器。现在这些问题已经得到改善,同时配合 Babel 等一些兼容性处理编译过程,我们可以更加关注网站的功能实现和优化。
从结构上来说,浏览器主要包括了八个子系统:用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统。
这些子系统组合构成了我们的浏览器,而谈到页面的加载和渲染,则离不开网络子系统、渲染引擎、JavaScript 解释器和浏览器引擎等。
如今大多数用户主要使用的浏览器包括两类:
下面我们以前端开发最常使用的 Chrome 浏览器为例(因为 Chrome 浏览器太牛啦,而且它们还要官方文章介绍做参考),进行更详细的介绍。
应该很多前端开发都知道,Chrome 浏览器使用了多进程架构,包括浏览器进程、渲染器进程、插件进程和 GPU 进程:
如今,基本上所有的浏览器都支持多个选项卡。在 Chrome 中,每个选项卡在单独的渲染器进程中运行,渲染器进程主要用于控制和处理选项卡中的网站内容显示。渲染器进程支持多线程,包括:
setTimeout
和setInterval
所在的线程选项卡之外的所有内容都由浏览器进程处理,浏览器进程则主要用于控制和处理用户可见的 UI 部分(包括地址栏,书签,后退和前进按钮)和用户不可见的隐藏部分(例如网络请求和文件访问)。浏览器进程同样支持多线程,包括:
这些线程其实我们在学习其他内容的时候也会涉及到,比如在页面的加载过程中,涉及 GUI 渲染线程与 JavaScript 引擎线程间的互斥关系,因此页面中的<script>
和<style>
元素设计不合理会影响页面加载速度。
除此之外,UI 线程、网络线程、存储线程、浏览器事件触发线程、浏览器定时器触发线程中 I/O 事件通过异步任务完成时触发的函数回调,解决了单线程的 Javascript 阻塞问题。结合 Event Loop 的并发模型设计,解决了 Javascript 中同步任务和异步任务的管理问题。
下面我们来介绍浏览器中页面的渲染过程,该部分内容同样基于 Chrome 浏览器,更加详细地介绍浏览器进程和线程如何通信来显示页面。
首先我们将浏览器中页面的渲染过程分为两部分:
前面我们介绍了一个 HTTP 的请求过程,该部分内容更倾向于将浏览器当成一个完整的对象,来介绍浏览器与外界的交互过程。
下面,我们来深入浏览器内部来进行分析,当用户在地址栏中输入内容时:
以上是用户在地址栏输入网站地址,到页面开始渲染的整体过程。如果当前页面跳转到其他网站,浏览器将调用一个单独的渲染进程来处理新导航,同时保留当前渲染进程来处理像unload
这类事件。
可以看到,页面导航的过程主要依赖浏览器进程。其中,上述过程中的步骤 5 便是页面的渲染部分,该过程同样依赖渲染器进程,我们一起来看看。
前面说过,渲染器进程负责选项卡内部发生的所有事情,它的核心工作是将 HTML、CSS 和 JavaScript 转换为可交互的页面。整体上,渲染器进程渲染页面的流程基本如下:
position
/overflow
/z-index
属性等计算大致流程如下图:
我们来分别看下。
渲染器进程的主线程会解析以下内容:
解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。
通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程其实便是我们常说的渲染树的创建过程。
在这个过程中,像header
或display:none
的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。
布局完成后,将会进入绘制环节。
在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。
需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(Reflow):
为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。
重排需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为可能会导致性能降低。
通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。
光栅化可以被 GPU 加速,光栅化后的位图会被存储在 GPU 内存中。根据前面介绍的渲染流程,当页面布局变更了会触发重排和重绘,还需要重新进行光栅化。此时如果页面中有动画,则主线程中过多的计算任务很可能会影响动画的性能。
因此,现代的浏览器通常使用合成的方式,将页面的各个部分分成若干层,分别对其进行栅格化(将它们分割成了不同的瓦片),并通过合成器线程进行页面的合成:
合成过程如下:
合成的真正目的是,在移动合成层的时候不用重新光栅化。因为有了合成器线程,页面才可以独立于主线程进行流畅的滚动。
到这里,页面才真正渲染到屏幕上。
我们在绘制页面的时候,也可能会遇到很多奇怪的渲染问题,比如使用了transform:scale
可能会导致某些浏览器中渲染模糊,究其原因则是由于光栅化过程导致的。像前面所说,前端开发需要频繁跟浏览器打交道,所谓知己知彼百战不殆,我们应该对其运行过程有更好的了解。
这里主要介绍了浏览器的组成和结构,并从浏览器内部分工角度来介绍页面的渲染过程。掌握页面的渲染过程,有利于我们进行一些性能优化,尤其如果涉及动画、游戏等频繁绘制的场景,渲染性能往往是需要不断进行优化的瓶颈。
对于介绍浏览器的渲染过程相关的内容,非常推荐大家参考两篇文章:
这篇文章也是参考了这两篇文章以及一些论文,以我自己的理解来进行总结输出,推荐大家也要阅读原文哦。
查看Github有更多内容噢: https://github.com/godbasin
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。