浏览器原理全面总结

本文详细阐述了现代浏览器架构,从单进程发展到多进程,涉及不同进程的功能划分,如浏览器进程、网络进程、GPU进程和插件进程。重点讲解了渲染进程和任务队列在协调JavaScript与HTML渲染中的作用,以及async和defer属性对JavaScript加载时机的影响。
摘要由CSDN通过智能技术生成

 浏览器架构:

早期浏览器架构: 单进程,多线程,带来的问题是效率低,不稳定,当一个线程崩溃,整个浏览器进程也崩溃

现代浏览器架构: 多进程,其主要进程包含:

        浏览器进程: 负责浏览器页面,用户交互,缓存,子进程管理

        网络进程: 负责网络资源请求

        GPU进程:负责网页的渲染

        插件进程: 负责如chrome浏览器中的插件,在沙箱(sandbox)中,如果崩溃不会影响浏览器

        渲染进程(重要!): 每个页面对应一个渲染进程,也称为浏览器的内核

其中,主流浏览器对应的内核如下:(面试可以简单记一下)

Chrome: Blink

Safari: webkit

IE: Trident

FireFox: Gecko


渲染进程(内核)

渲染是什么? 

HTML字符串 -> 屏幕上每个像素的颜色信息

GUI渲染引擎 & javascript解析引擎

渲染进程包含一个主线程,这个主线程上工作着 GUI渲染引擎 和 javascript解析引擎。其中GUI渲染引擎负责解析html字符串,javascript引擎负责解析javascirpt代码(chrome浏览器中的V8)

GUI渲染引擎和javascript引擎互斥的工作在主线程上,当渲染引擎工作时,javascript引擎会被阻塞无法工作,反之亦然,当javascipt引擎释放主线程时,渲染引擎才工作,也就是js运行和页面渲染是互斥的,这也解释了为什么js会阻塞网页渲染。

为什么要互斥?

1. 设计上因为GUI渲染引擎和js解析引擎运行在主线程中,无法脱离宿主

2. 用途上,因为要操作DOM,多线程运行会导致出现临界资源的问题,需要互斥锁,信号量等等机制来解决 很复杂。


任务队列

为了协调渲染引擎和js解析引擎的工作,引入一个任务队列(event loop),任务队列相当于中间人在浏览器地址栏输入url之后,主进程中的ui线程会判断是否为url,如果是则让网络进程去处理并且请求资源。

请求到的资源 (html)是一串字符串,会包装成一个(C++)渲染任务,并且放到任务队列,此时GUI渲染引擎会从任务队列中取出任务并且执行html的解析和渲染,当解析到<script>标签(不考虑defer和async)会将scipr中的js代码包装成任务放到任务队列并挂起,此时js解析引擎来解析执行js代码。当js执行完其中的所有同步代码,也会挂起。

当js遇到一个异步任务如

// 宏任务
settimeout(()=>{
    console.log(xxxx)
},1000)

//微任务
Promise.resolve(()=>{
    console.log(xxx)
})

// 网络请求
fetch(url).then((res)=>{
    console.log(res)
})

也会将任务交给浏览器中其他的进程处理,如计时器,网络进程等等,当这些任务执行完成后,会将相应的callback包装成任务放到任务队列中,排队执行。我们可以通过将代码包装到异步任务中的方式挂起js解析引擎,让出主线程,对于一些运算量大的代码可以分段处理,防止渲染卡顿,白屏,影响用户体验。

现代浏览器由多个任务队列

        微任务队列 优先级最高

        交互队列 优先级高 

        延时队列 优先级 中


渲染引擎工作流程

html&css 解析

html和css的解析是并行发生的,其中主线程中的渲染引擎会解析html字符串,同时主线程之外的预解析线程会扫描文档找到下载并且解析css代码,这样有利于提升解析速度。

渲染引擎会线创建一个Document对象(C++对象)作为根节点,然后根据html的结构,创建DOM(Document Object Module 文档对象模型 )树。

注意 DOM是C++对象构成的树,js是在上面做了一层包装,让我们可以通过js访问DOM节点

由于css的解析和dom的解析不干扰 ,为了同时解析,Css解析不在主线程,会生成CSSOM树, 也是C++对象 其子节点是所有css来源,通过js在document.cssStyleSheets 可以访问。

具体流程如下图所示:也就是到attachment的步骤

通过上图可以看到 css的解析,不干扰html的解析,但是会影响下面的html渲染,因为cssom树要和dom树合并成布局树,后面的步骤会受到css下载解析渲染的影响。

同时,因为js中可以访问css对象,如document.style 所以js的解析运行必须要在css解析之后进行,也就是说 css的解析会影响js的解析运行

样式计算

html解析生成dom树 ,css解析生成cssom树,将HTML解析生成的 DOM树遍历,结合CSSOM树,对每个节点计算出最终样式,生成一棵带有最终样式(Computed Style 计算后的样式)的DOM树。样式计算之后,所有的样式属性 都必须有value,如图所示: 

布局Layout     

布局阶段就是将样式计算之后的树,做一些修改,保留包含有几何信息的元素,并且增加一些dom中不包含的元素 如伪元素,匿名行内元素等。

渲染树(布局树)中仅仅包含有几何信息的元素,比如:

  1. 比如display: none的元素,这些元素在布局树中就没有 因为没有空间,但是visbility: none这会有 因为只是看不见 但是空间还有(底层原理)
  2. 再比如伪元素,在DOM中无法找到,就是在布局阶段挂在布局树上了,因为其有几何信息
  3. 内容必须在行盒中,如果内容在块盒中,则会包裹一个匿名行内元素,这个元素也会挂在layout树上,但是在DOM中也是找不到的
  4. 行和块盒不能相邻,所以会创建匿名块盒也是放到布局树中,在dom中看不到

可以简单的观察一下,在浏览器中的元素检察中,是无法看到伪元素,匿名行内元素等等的,因为其在布局树不在dom树上面。

注意,布局树也是C++对象,但是不提供类似于cssom和dom树这样js包装提供访问的方式。 只能通过类似于clientWidth clientHeight这样的方式间接访问。

分层

为了提高效率,会对页面进行分层,将经常渲染的部分进行拆分,以避免发生变动(如reflow)之后要全部修改,比如滚动条就是分层的 如下图所示:

使用will-change opacity等 可以改变分层策略

绘制 paint 

根据每一层布局树的几何信息,生成绘制指令,注意这里类似于canvas,不是每个像素点的颜色信息,而是一些描述了如何绘画的信息(举个例子,比如向右移动xxx像素 在向下移动xxx个像素画线)

此时,渲染引擎在主线程中的任务完成,总结其任务包含 解析html -> 样式计算 -> 布局 -> 分层 -> 绘制 生成绘制指令

合成 (合成线程)

绘制指令会被交给渲染进程中的另一个线程: 合成线程进行处理

合成线程会在线程池中启动多个线程完成分块工作,会把大的网页分块,然后将对应的小块显示在viewpoint视口上

光栅化 (GPU进程 - 注意在渲染进程之外)

光栅化也就是将分好块的绘制指令转换成位图,也就是对应每个像素点的颜色信息,此过程中,合成线程会将分好块的绘制指令交给渲染进程之外的GPU进程来处理,GPU进程处理完成后会将位图信息返回给合成线程。

 

合成线程会将位图给gpu进程,gpu进程操作硬件(显卡)

为什么要走GPU进程做中转 不麻烦吗?
这里注意 合成线程在渲染进程,渲染进程在沙箱内 sandbox 没有直接调用硬件的能力
(第三方插件进程和渲染进程都在沙箱内 保证一个进程出错不会影响整个浏览器的运行!)

此时,网页内容就被显卡渲染到浏览器上了,渲染结束。


优化

transform  滚动

对于transform动画和滚动页面,会直接走最后一步,也就是直接重新画,因为不需要重新生成前面的信息了,这也是为什么滚动和transform动画的效率很高

可以在html中加入一段死循环的同步js代码做实验,即便页面卡死了,渲染线程不工作,页面也能完成transform动画和滚动,因为只走最后一步,页面卡死不影响画的过程。

重排(回流/reflow)和 重绘(repaint)

重排: resize页面,修改了几何信息,修改了dom之间的排版,此时需要从布局开始 -> 分层 -> 绘制 -> 分块 -> 光栅化 -> 画后面所有的步骤 (重新生成布局树)

所以尽可能不要用js频繁修改元素的尺寸,如<AutoHeightContianer/>开销就很大 尽量少用

repaint(重绘) 开销更低

        如果变化的元素,只是更改了元素的背景色,文字颜色、边框颜色等等不影响它周围或者内部布局的属性,那这种行为只会引起repaint(重绘) 走绘制 -> 分块 -> 光栅化 -> 画后面所有的步骤,不重新生成布局树,所以repaint的速度明显比reflow快

对比
  1. 重排一定会触发重绘,重绘未必触发重排(只改了背景 颜色 等等)
  2. 修改布局,修改元素位置,添加删除元素,尺寸改变,内容改变,窗口resize等等修改布局layout会触发重排reflow
  3. 修改属性, 修改元素的背景色、边框颜色等样式属性,添加或删除元素的伪类,如:hover等,操作canvas、SVG等图形元素。 触发重绘

合并渲染

如果在javasciript中修改了元素的几何信息,比如修改了元素的宽高,会生成一个渲染任务加入到任务队列中,并且在当前js代码执行完成后,js引擎挂起释放主进程后,会轮训检查任务队列并且让渲染引擎介入完成重新渲染,但是如果频繁变更几何信息,如:

dom.style.width = '1000px'
dom.style.height = '2000px'
dom.style.marginTop = '20px'

浏览器为了优化性能,不会生成三个渲染任务

浏览器会维护一个渲染队列,每次对布局有修改,都会push进队列中,等到队列数量够多或者达到一定时间flush队列,生成一个渲染任务,加入到事件循环中,等待js引擎让出主线程后,渲染引擎接入,完成渲染。

但是,如果我们直接读取布局树信息 如.clientWidth,由于更新渲染还没有执行,为了保证读取的正确性,浏览器会将保存在flush队列中的值返回,并且强制flush队列生成一个渲染任务,所以

dom.style.width = '1000px'
// 读区几何信息
console.log(dom.clientWidth)

dom.style.height = '2000px'
// 读区几何信息
console.log(dom.clientHeight)

dom.style.marginTop = '20px'
// 读区几何信息
console.log(dom.clientWidth)

此代码会生成三个渲染任务,浏览器的合并优化就不起作用,导致三次reflow,影响性能。

所以尽量少的去读区布局树信息,因为每次读取,都会强制flush渲染队列,生成任务,导致重排数量增加。

总结:

  1. 不要在js中频繁读区样式
  2. 如果要修改样式尽可能的集中修改
  3. 尽可能使用 transform实现动画效果 (这个只设计画的这个步骤 包括滚动条滚动)

javascript资源 script async defer

我们知道,使用<script/> 直接引入javascript代码会阻塞渲染引擎的渲染,因为js代码是可以动态的修改前面的dom结构,这可能使dom解析渲染没有意义,所以正常情况下,渲染引擎会等待js代码执行结束之后继续解析渲染。

这样可能导致一些问题,由于js的下载,解析,执行,都需要耗费时间,此时主线程被占用,渲染引擎无法工作,会造成页面白屏,用户体验很差。

如果js代码中包含了同步的delay如:

    <div>hello</div>
    <div>hello</div>
    <div>hello</div>
    <div>hello</div>
    <div>hello</div>
    <div>hello</div>
    <div>hello</div>
    <script>
        const delay = (times) => {
            const startTime = new Date().valueOf()
            while (new Date().valueOf() - startTime <= times) {
                console.log('delay')
            }
        }
        // 同步delay
        delay(1000)
    </script>

此时js引擎占据主线程,渲染引擎也被挂起无法进入,页面会白屏,会堵在attachment之前,导致上面的hello也展示不出来。 

同时这种方式也会造成,如果在html中间引入js代码,那么js代码无法获取下方的dom元素,所以一般会将script标签放到html body的最下方,但是造成的问题是,在html代码都解析完成之后再去下载,解析,执行js,也会增加渲染的时间,能否让下载提前的情况下,也能最后运行js代码

所以引入async和defer,注意defer和async都无法作用域inline的script 必须是引用外部的js文件才起作用

async

当浏览器遇到带有 async 属性的 script 时,请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML,一旦网络请求回来之后,如果此时 HTML 还没有解析完,浏览器会暂停解析,先让 JS 引擎执行代码,执行完毕后再进行解析,图示如下:

当然,如果在 JS 脚本请求回来之前,HTML 已经解析完毕了,那就啥事没有,立即执行 JS 代码,如下图所示:

所以 async 是不可控的,因为执行时间不确定,你如果在异步 JS 脚本中获取某个 DOM 元素,有可能获取到也有可能获取不到。而且如果存在多个 async 的时候,它们之间的执行顺序也不确定,完全依赖于网络传输结果,谁先到执行谁。

defer 

当浏览器遇到带有 defer 属性的 script 时,获取该脚本的网络请求也是异步的,不会阻塞浏览器解析 HTML,一旦网络请求回来之后,如果此时 HTML 还没有解析完,浏览器不会暂停解析并执行 JS 代码,而是等待 HTML 解析完毕再执行 JS 代码,图示如下:

如果存在多个 defer script 标签,浏览器(IE9及以下除外)会保证它们按照在 HTML 中出现的顺序执行,不会破坏 JS 脚本之间的依赖关系。

最后,根据上面的分析,不同类型 script 的执行顺序及其是否阻塞解析 HTML 总结如下:

script 标签

JS 执行顺序

是否阻塞解析 HTML

<script>

在 HTML 中的顺序

阻塞

<script async>

网络请求返回顺序

可能阻塞,也可能不阻塞

<script defer>

在 HTML 中的顺序

不阻塞

注意 defer还是会影响dom的渲染过程的,但是不会阻塞dom tree生成的过程,如下代码,页面依旧无法生成出来,因为在DomContentLoaded事件之前就被卡住了

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值