目录

相关进程

具体步骤

一 url解析

二 发起请求

三 准备渲染进程

四 提交响应数据

五 渲染

总结


根据之前看的很多文章和做的一点笔记,还是打算整理整理做个总结。其中对渲染前的很多准备工作也进行了详述,涉及一些计算机网络知识、操作系统知识,只做了解即可。整体分成五步

        1解析url

        2 网络请求:由网络进程发起请求资源和接收数据

        3 准备渲染:浏览器给页面分配渲染进程,并初始化一些准备工作

        4 提交响应数据:网络进程将响应数据传输给渲染进程,且浏览器进程会更新页面状态

        5 渲染:构建DOM树、计算样式、计算布局、进行绘制、合成显示(都由渲染进程完成)

其中最复杂的就是渲染这一步,花了最多篇幅详述整个渲染过程,可着重研究这一步。第2步网络请求这里,涉及dns查询、建立tcp连接这些知识,也是前端经常考查的知识。

相关进程

在整个过程中,涉及以下三个进程,三个进程之间交互完成所有工作:

浏览器进程

网络进程

渲染进程

具体步骤

一 url解析

在地址栏输入url地址后,该内容首先被地址栏解析,解析由浏览器进程下面的UI线程负责。

具体解析流程:

二 发起请求

首先,经过解析后的url地址,会被浏览器进程以进程间通信的方式发送给网络进程

1 网络进程收到http请求后:

    (1)首先会检查本地缓存是否存在该资源,如果存在则直接将该资源返回给浏览器进程,那么该次url导航也就结束了

    (2)如本地缓存中不存在该资源,网络进程才会正式发起http请求

        http请求的具体过程:

                i. DNS查询(计网知识,就不详细展开了,主要就是递归与迭代配合的方式进行查询)

                ii. DNS查询结束后,获取到了对应ip,这时候就开始建立TCP请求了(如果请求协议是https,在此之前,还要建立TLS连接)

                iii. 建立完连接,浏览器端会构建请求行、请求头等信息,如果有相关的cookie,根据实际会将cookie数据添加到请求头中

                iv. 准备完成,开始发送请求

         服务器收到请求后,会根据请求信息生成响应数据,然后发送给网络进程(成功的话)

2 网络进程接收到服务器发来的响应数据后:

        如果状态码是301或302,则表示重定向,网络进程根据响应头中的Location字段,(该字段就是重定向的url),重新发起http请求,没错,重新来一遍;

        如果状态码是200,那么说明请求资源成功并且成功接收

        成功接收到服务器返回的响应数据,还得判断返回的数据类型,http头中的Content-Type字段会指明返回的响应数据是什么类型;

          Content-Type字段举例:

          'text/plain' :代表这是普通文本,不会产生HTML解析

          'text/'html' :代表这是HTML格式内容,就是要进行渲染的所依据的HTML字节流

          'application/octet-stream' :代表这是字节流类型,通常浏览器将其视作下载类型处理

        如果响应数据被判断为下载类型,那么浏览器会将其交给下载管理器处理,那么这次url导航就结束了;如果是HTML格式的内容,接下来浏览器就该准备渲染工作

三 准备渲染进程

以Chrome为例,浏览器默认情况下会为每个页面分配一个渲染进程,所以说每打开一个页面,这个页面也就获得了一个渲染进程,当然也有例外;当两个页面从属于一个站点时(同站点即这两个页面的url,协议一样,根域名也一样,那么这两个页面就从属于同一个站点),后打开的页面会复用先打开的页面的渲染进程。

渲染进程准备完毕后,就等着响应数据提交过来(因为现在响应数据还在网络进程里),然后开始渲染工作。

四 提交响应数据

这里又要涉及进程间通信:

    1 网络进程收到响应数据后,会告知浏览器进程,然后由浏览器进程通知渲染进程:数据准备完毕,你可以接收啦

    2 渲染进程收到通知,会和网络进程间建立一条传输数据的“管道”

    3 接下来,通过这条“管道”,网络进程和渲染进程完成交接,然后渲染进程会发送给浏览器进程一个“确认接收到响应数据”的消息

    4 浏览器进程收到渲染进程发来的确认消息后,会更新浏览器页面状态:包括安全状态、地址栏的url、前进后退的历史状态、更新web页面(这部分之后要着重学习整理下)

然后,就可以开始渲染了!但是!进入渲染这个阶段,不代表渲染是一下就完成的,还要很多渲染前的必备的事情要做。下面继续细说渲染过程。回答到这里,大致清晰的流程已经出来了,如果不深入渲染过程,记住后面是渲染过程就OK了。

五 渲染

1 DOM树的构建

解析HTML是在渲染进程的主线程进行

          说明:渲染引擎内,有一个叫HTML解析器的模块,该模块负责将HTML字节流转换为对应的DOM结构

    (1)词法分析:HTML字节流首先通过分词器以词法分析的方式,被转换为一个个Token,Token包括Tag Token文本Token,其中Tag Token包括StartTag TokenEndTag Token

          说明:HTML解析器维护一个Token栈的结构,该栈用于计算DOM中各节点间的父子关系

    (2)通过分词器后转换出来的Token,经过以下处理,生成DOM节点

            i. 如果该Token是一个StartTag Token,那么将其压入栈中,同时HTML为该Token创建一个DOM节点,并将该节点加入到DOM树中;

            ii. 如果该Token是一个文本Token,则不需入栈,同时HTML为其创建DOM节点并加入DOM树,此时该文本节点的父节点即当前栈顶Token所对应的节点;

            iii. 如果该Token是一个EndTag Token,则检查当前栈顶Token是否是与其对应的StartTag Token,是则将栈顶StartTag Token弹出,到此表明一个标签解析完成(比如<div></div>)

    词法分析和DOM节点生成就是这么不断进行下去,并且是同步进行的,直到所有的HTML字节流都被解析完成,此时,DOM树也就构建好了。

        这里要注意!!!这个HTML解析,不是等到渲染进程接收完所有的HTML字节流(也就是网络进程传输给渲染进程的响应数据)才开始的,而是网络进程传多少,渲染进程接收到多少,HTML也就解析多少,这些步骤实际上都是同时进行的

2 CSSOM和渲染树

DOM树构建好,还不能直接渲染,因为DOM节点对应的样式还没有加进来(前提是有CSS样式)。HTML解析过程中,碰到CSS样式,会去下载,然后解析得到CSSOM。下载CSS不阻塞HTML解析,但是解析“可能”阻塞

这样理解“可能”阻塞:<link> 标签不会像同步 <script> 一样使 HTML Parser 主动暂停。浏览器遇到 <link> 后,会立即通知网络线程下载 CSS,然后继续解析后续 HTML,因此通常说"CSS 下载不阻塞 HTML 解析"。CSS 文件下载完成后,需要在渲染进程主线程上解析生成 CSSOM,这个解析过程会短暂占用主线程,因此在极端情况下可能与 HTML Parser 竞争主线程资源。但这与同步 <script> 的"规范要求暂停 HTML 解析"是不同的概念。此外,为了保证 JavaScript 能获取正确的样式信息,浏览器会在必要时等待 CSSOM 构建完成后再执行后续脚本,所以常说 CSS 会阻塞 JavaScript,而不是阻塞 HTML 解析。

解析CSS也是在渲染进程的主线程进行

构建好的DOM树和解析完的CSSOM,会经历过滤+样式计算的过程,从 DOM 树和 CSSOM 树到渲染树(Render Tree),核心机制可以概括为:遍历 DOM 树的可见节点,并在 CSSOM 中匹配样式,生成一个仅包含可见元素及其计算样式的树结构。

Tips:DOM树和渲染树并非一一对应,一个 DOM 节点,不一定只对应一个渲染树节点,也可能不生成渲染树节点。比如display:none的节点在DOM树中,但不在渲染树中;伪元素(::before / ::after):它们不在 DOM 树中,但会出现在渲染树中。

计算CSS样式大概分为3步,这3步就不详细展开了,因为比较复杂(自己也不是很懂),大概是下面这样:

        (1)将CSS转换为浏览器的能够理解的结构,稍微具体一点就是:渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets

        (2)转换样式表中的属性值,使其标准化

        (3)计算出DOM树中每个节点的具体样式

3 布局计算

计算布局还是是在渲染进程的主线程进行

计算各节点在页面中的具体位置,也就是怎么排放这些DOM节点

步骤如下:

    (1)拿着上一步生成的渲染树,再加上视口尺寸(即浏览器内容区的宽高,由窗口大小决定),开始进行计算。

    (2)确定尺寸、确定坐标

    (3)生成布局树

4 绘制

网上很多文章,比较笼统,即布局之后就是绘制:

    (1)接下来就是对布局树进行分层,生成图层树

            这里简单提一下图层树,这又涉及CSS中“层叠上下文”的知识,不展开了。通常情况下,一个DOM节点对应一层,但有的节点没有自己的图层,则从属于其父节点的图层。

           分配规则,满足以下两点其中之一的节点,会被分配到一个图层:

                 i.拥有层叠上下文属性的元素;

                ii.需要剪裁的地方也会被创建为图层(比如div中的一段文字,文字占地尺寸超出了div尺寸,那么这时候会发生剪裁,会为文字创建一个新的图层

    (2)渲染引擎内部,根据图层树中各个图层,会进行绘制。

关于绘制:

  • 执行者渲染进程的主线程(Main Thread)

  • 输入:生成的图层树(Layer Tree)(每个图层包含布局计算好的几何信息)。

  • 过程:主线程遍历每一个图层,记录“绘制动作”的顺序。比如:“先画背景色为白色 -> 再画边框为 1px 黑色 -> 再画文字‘Hello’位于 (10, 20) 处”。

  • 输出:每个图层对应的 绘制指令列表(Paint Records / Display Lists)。此时还没有生成任何像素图,只是一堆纯文本形式的绘图指令。

5 合成显示

渲染引擎完成绘制工作后,还要进行分块光栅化,最后是合成。然后显示到屏幕上。

关于分块

  • 执行者合成线程(Compositor Thread)(注意:此时已经脱离了主线程,这也是为什么滚动不卡顿的原因)。

  • 输入:绘制指令列表(Display Lists)和图层尺寸。

  • 过程:合成线程发现一个图层可能非常大(比如一张 5000px 长的长图),如果一次性交给 GPU 去处理,内存会爆掉。因此它会把图层切割成无数个固定大小的小瓦片(Tiles)(通常为 256x256 或 512x512 像素)。

  • 输出:按网格切分好的瓦片坐标集合(Tile Coordinates)

关于光栅化

  • 执行者光栅化线程池(Raster Thread Pool)(位于渲染进程内的多线程工作池)。

  • 输入:特定瓦片的绘制指令(比如“画个红方块在坐标 0,0”)和该瓦片的坐标。

  • 过程:这是将向量(指令)转为栅格(像素/位图) 的物理计算过程。GPU 或 CPU 将这些指令渲染成填充了 RGBA(红绿蓝透明度)颜色的像素点,存入内存中的纹理(Texture)

  • 输出:存储在 GPU 内存或系统内存中的位图(Bitmap / 纹理数据)。至此,页面的内容变成了计算机能识别的“图片帧”。

关于合成

  • 执行者合成线程

  • 输入

    • 图层纹理(Textures):光栅化线程池产出的像素位图(已存储在 GPU 内存中)。

    • 图层属性(Layer Properties):每个图层的 transform(位移/旋转/缩放)、opacity(透明度)、filter(滤镜)、clip(裁剪边界)以及滚动偏移量(scrollTop)。

    • 图层层级树(Layer Tree):明确哪个图层在上层(z-index),哪个在下层。

    • 视口信息(Viewport):当前屏幕的宽高和可见区域坐标(用于裁剪)。

  • 过程:对输入数据进行“几何运算与堆叠”

  • 输出绘图指令包,并提交给GPU进程

最后

因为最近做的一个项目,没想到过了好久,又要重新捡起这部分知识

参考了很多文章后,加上我个人的理解,大概地解释了下,只是“大概”,因为这个问题涉及的知识实在是太多了!以上应该也只是总结了一半,要深入的代价是头秃,先不说渲染这个过程有多繁琐,光是所涉及操作系统中进程和线程相关的概念都需要好好整理一遍

但是从输入url到页面渲染,大致的流程参考以上具体步骤已经能解答大部分疑惑了

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐