文章目录
  1. 1. 认识浏览器
    1. 1.1. 浏览器的结构
    2. 1.2. Chrome 多进程架构
  2. 2. 浏览器中页面的渲染过程
    1. 2.1. 1. 页面导航
    2. 2.2. 2. 页面渲染
      1. 2.2.1. 解析
      2. 2.2.2. 布局
      3. 2.2.3. 绘制
      4. 2.2.4. 光栅化
  3. 3. 结束语
    1. 3.1. 参考

作为前端开发,我们的日常工作中除了写代码之外,几乎大多数的时间都在跟浏览器打交道。当然,现在我们甚至写代码都可以直接在浏览器里完成,一个浏览器走天下。

因此,我们应该对浏览器的了解要更加深入,除了了解怎么使用和调试浏览器,我们还要掌握它是怎样将我们编写的代码渲染到页面中的。

认识浏览器

浏览器的主要功能,是通过向服务器请求并在浏览器窗口中展示 Web 资源内容,通常包括 HTML 文档、PDF、图片等,我们也可以通过插件的方式加载更多其他的资源类型(比如播放视频)。

对于浏览器的问题,HTTP 请求相关的,想必各位在面试的时候都被问烂了吧,这里直接过一下浏览器中的 HTTP 请求过程:

  1. DNS 域名解析(此处涉及 DNS 的寻址过程),找到网页的存放服务器。
  2. 浏览器与服务器建立 TCP 连接。
  3. 浏览器发起 HTTP 请求。
  4. 服务器响应 HTTP 请求,返回该页面的 HTML 内容。
  5. 浏览器解析 HTML 代码,并请求 HTML 代码中的资源(如 JavaScript、CSS、图片等,此处可能涉及 HTTP 缓存)。
  6. 浏览器对页面进行渲染呈现给用户。

这篇文章会重点介绍第 6 步,该步骤涉及浏览器的渲染过程和原理。除了初次加载页面,用户的很多操作都同样涉及到浏览器渲染,比如以下功能:

  • 地址栏输入 URL
  • 点击刷新和停止按钮,控制页面加载
  • 点击后退和前进按钮,快速实现页面跳转
  • 书签和收藏,快速打开页面

除了这些,实际上我们和浏览器的几乎所有操作,都涉及到浏览器的渲染过程。为了更深刻地认识这些过程,我们先来认识下浏览器的结构。

HTML 和 CSS 规范中规定了浏览器解析和渲染 HTML 文档的方式,曾经各个浏览器都只遵循其中一部分,因此前端开发经常需要兼容各种浏览器。现在这些问题已经得到改善,同时配合 Babel 等一些兼容性处理编译过程,我们可以更加关注网站的功能实现和优化。

浏览器的结构

从结构上来说,浏览器主要包括了八个子系统:用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统。

这些子系统组合构成了我们的浏览器,而谈到页面的加载和渲染,则离不开网络子系统、渲染引擎、JavaScript 解释器和浏览器引擎等。

如今大多数用户主要使用的浏览器包括两类:

  • 台式机:Chrome、Internet Explorer、Firefox、Safari、Opera 等
  • 移动设备:Android 浏览器、iPhone、Opera Mini、Opera Mobile、UC 浏览器、Chrome 等。

下面我们以前端开发最常使用的 Chrome 浏览器为例(因为 Chrome 浏览器太牛啦,而且它们还要官方文章介绍做参考),进行更详细的介绍。

Chrome 多进程架构

应该很多前端开发都知道,Chrome 浏览器使用了多进程架构,包括浏览器进程、渲染器进程、插件进程和 GPU 进程:

如今,基本上所有的浏览器都支持多个选项卡。在 Chrome 中,每个选项卡在单独的渲染器进程中运行,渲染器进程主要用于控制和处理选项卡中的网站内容显示。渲染器进程支持多线程,包括:

  • GUI 渲染线程:负责对浏览器界面进行渲染
  • JavaScript 引擎线程:负责解析和执行 JavaScript 脚本
  • 浏览器定时器触发线程:setTimeoutsetInterval所在的线程
  • 浏览器事件触发线程:该线程负责处理浏览器事件,并将事件触发后需要执行的代码放置到 JavaScript 引擎中执行

选项卡之外的所有内容都由浏览器进程处理,浏览器进程则主要用于控制和处理用户可见的 UI 部分(包括地址栏,书签,后退和前进按钮)和用户不可见的隐藏部分(例如网络请求和文件访问)。浏览器进程同样支持多线程,包括:

  • UI 线程:用于绘制浏览器的按钮和输入字段
  • 网络线程:用于处理网络请求,以及从服务器接收数据
  • 存储线程:用于控制对文件的访问

这些线程其实我们在学习其他内容的时候也会涉及到,比如在页面的加载过程中,涉及 GUI 渲染线程与 JavaScript 引擎线程间的互斥关系,因此页面中的<script><style>元素设计不合理会影响页面加载速度。

除此之外,UI 线程、网络线程、存储线程、浏览器事件触发线程、浏览器定时器触发线程中 I/O 事件通过异步任务完成时触发的函数回调,解决了单线程的 Javascript 阻塞问题。结合 Event Loop 的并发模型设计,解决了 Javascript 中同步任务和异步任务的管理问题。

下面我们来介绍浏览器中页面的渲染过程,该部分内容同样基于 Chrome 浏览器,更加详细地介绍浏览器进程和线程如何通信来显示页面。

浏览器中页面的渲染过程

首先我们将浏览器中页面的渲染过程分为两部分:

  1. 页面导航:用户输入 URL,浏览器进程进行请求和准备处理。
  2. 页面渲染:获取到相关资源后,渲染器进程负责选项卡内部的渲染处理。

1. 页面导航

前面我们介绍了一个 HTTP 的请求过程,该部分内容更倾向于将浏览器当成一个完整的对象,来介绍浏览器与外界的交互过程。

下面,我们来深入浏览器内部来进行分析,当用户在地址栏中输入内容时:

  1. 首先浏览器进程的 UI 线程会进行处理:如果是 URI,则会发起网络请求来获取网站内容;如果不是,则进入搜索引擎。
  2. 如果需要发起网络请求,请求过程由网络线程来完成。HTTP 请求响应如果是 HTML 文件,则将数据传递到渲染器进程;如果是其他文件则意味着这是下载请求,此时会将数据传递到下载管理器。
  3. 如果请求响应为 HTML 内容,此时浏览器应导航到请求站点,网络线程便通知 UI 线程数据准备就绪。
  4. 接下来,UI 线程会寻找一个渲染器进程来进行网页渲染。当数据和渲染器进程都准备好后,HTML 数据通过 IPC 从浏览器进程传递到渲染器进程中。
  5. 渲染器进程接收 HTML 数据后,将开始加载资源并渲染页面。
  6. 渲染器进程完成渲染后,通过 IPC 通知浏览器进程页面已加载。

以上是用户在地址栏输入网站地址,到页面开始渲染的整体过程。如果当前页面跳转到其他网站,浏览器将调用一个单独的渲染进程来处理新导航,同时保留当前渲染进程来处理像unload这类事件。

可以看到,页面导航的过程主要依赖浏览器进程。其中,上述过程中的步骤 5 便是页面的渲染部分,该过程同样依赖渲染器进程,我们一起来看看。

2. 页面渲染

前面说过,渲染器进程负责选项卡内部发生的所有事情,它的核心工作是将 HTML、CSS 和 JavaScript 转换为可交互的页面。整体上,渲染器进程渲染页面的流程基本如下:

  • 解析(Parser):解析 HTML/CSS/JavaScript 代码
  • 布局(Layout):定位坐标和大小、是否换行、各种position/overflow/z-index属性等计算
  • 绘制(Paint):判断元素渲染层级顺序
  • 光栅化(Raster):将计算后的信息转换为屏幕上的像素

大致流程如下图:

浏览器构造渲染树流程

我们来分别看下。

解析

渲染器进程的主线程会解析以下内容:

  • 解析 HTML 内容,产生一个 DOM 节点树
  • 解析 CSS,产生 CSS 规则树
  • 解析 Javascript 脚本。由于 Javascript 脚本可以通过 DOM API 和 CSSOM API 来操作 DOM 节点树和 CSS 规则树,因此该过程中会等待 JavaScript 运行完成才继续解析 HTML

解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。

布局

通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程其实便是我们常说的渲染树的创建过程。

在这个过程中,像headerdisplay:none的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。

布局完成后,将会进入绘制环节。

绘制

在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。

需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(Reflow):

  • 重绘:屏幕的一部分要重画,比如某个 CSS 的背景色变了,但是元素的几何尺寸没有变
  • 重排:元素的几何尺寸变了(渲染树的一部分或全部发生了变化),需要重新验证并计算渲染树

为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。

重排需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为可能会导致性能降低。

光栅化

通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。

光栅化可以被 GPU 加速,光栅化后的位图会被存储在 GPU 内存中。根据前面介绍的渲染流程,当页面布局变更了会触发重排和重绘,还需要重新进行光栅化。此时如果页面中有动画,则主线程中过多的计算任务很可能会影响动画的性能。

因此,现代的浏览器通常使用合成的方式,将页面的各个部分分成若干层,分别对其进行栅格化(将它们分割成了不同的瓦片),并通过合成器线程进行页面的合成:

合成过程如下:

  1. 当主线程创建了合成层并确定了绘制顺序,便将这些信息提交给合成线程。
  2. 合成器线程将每个图层栅格化,然后将每个图块发送给光栅线程。
  3. 光栅线程栅格化每个瓦片,并将它们存储在 GPU 内存中。
  4. 合成器线程通过 IPC 提交给浏览器进程,这些合成器帧被发送到 GPU 进程处理,并显示在屏幕上。

合成的真正目的是,在移动合成层的时候不用重新光栅化。因为有了合成器线程,页面才可以独立于主线程进行流畅的滚动。

到这里,页面才真正渲染到屏幕上。

我们在绘制页面的时候,也可能会遇到很多奇怪的渲染问题,比如使用了transform:scale可能会导致某些浏览器中渲染模糊,究其原因则是由于光栅化过程导致的。像前面所说,前端开发需要频繁跟浏览器打交道,所谓知己知彼百战不殆,我们应该对其运行过程有更好的了解。

结束语

这里主要介绍了浏览器的组成和结构,并从浏览器内部分工角度来介绍页面的渲染过程。掌握页面的渲染过程,有利于我们进行一些性能优化,尤其如果涉及动画、游戏等频繁绘制的场景,渲染性能往往是需要不断进行优化的瓶颈。

参考

对于介绍浏览器的渲染过程相关的内容,非常推荐大家参考两篇文章:

这篇文章也是参考了这两篇文章以及一些论文,以我自己的理解来进行总结输出,推荐大家也要阅读原文哦。

码生艰难,写文不易,给我家猪囤点猫粮了喵~

B站: 被删

查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢

如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢

作者:被删

出处:https://godbasin.github.io

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章目录
  1. 1. 认识浏览器
    1. 1.1. 浏览器的结构
    2. 1.2. Chrome 多进程架构
  2. 2. 浏览器中页面的渲染过程
    1. 2.1. 1. 页面导航
    2. 2.2. 2. 页面渲染
      1. 2.2.1. 解析
      2. 2.2.2. 布局
      3. 2.2.3. 绘制
      4. 2.2.4. 光栅化
  3. 3. 结束语
    1. 3.1. 参考