浏览器

526 阅读23分钟

一、宏观视角下浏览器

01 打开一个页面四个进程

进程和线程: 一个进程就是一个程序运行的运行实例,当启动一个程序的时候,操作系统为该程序创建一块内存用来存放代码、运行中的数据和一个执行任务的主线程,这样的运行环境叫“进程”。 线程是依附于进程的,进程中使用多线程并行处理能提升效率。 目前浏览器的多进程架构:

image.png

  • 浏览器进程:界面显示,交互,子进程管理和存储等。
  • 渲染进程:解析html css和js,排版引擎blink和js的v8引擎都是运行在该进程中,chrome会为每个tab标签页创建一个渲染进程,出于安全考虑(资源都是网络进程来的,可能不安全),渲染进程都是沙箱模式下运行的
  • GPU进程:绘制chrome的UI界面都采用GPU进程。
  • 网络进程:网络资源加载
  • 其他插件进程等:因插件容易崩溃等问题,单独放在进程中。

Q&A: 即使是多进程的架构,还是会有一些由于页面卡死最终奔溃导致所有页面崩溃的情况? 通常情况下,一个页面是一个进程,但是有一种情况是,同一站点(same site)。chrome的默认策略是,每个标签对应一个渲染进程,但是如果从一个页面打开了新页面,新页面和当前属于同一站点,新页面复用父页面的渲染进程,这个策略叫process-per-site-instance。所以这种情况下如果一个页面崩溃,因为他们使用了同一个渲染进程。

扩展: 浏览器的UA发展史【后续贴链接】 Chrome Tools

02 基于TCP/IP的HTTP

IP协议: IP(Internet Protocol),互联网通信协议 TCP协议: TCP(Transmission Control Protocol,传输控制协议),面向连接的、可靠的,基于字节流的传输层通信协议。 HTTP: 建立在TCP连接基础上的web协议。浏览器的HTTP协议是基于TCP/IP的应用层协议。

客户端发起HTTP请求流程:

  1. 构建请求: 构建请求行信息,准备发起网络请求
  2. 查找缓存: 在发起网络请求前,浏览器会先在浏览器缓存中查询是否有要请求的文件
  3. 准备IP地址和端口:这时候去请求的DNS返回域名IP,这里也会有DNS数据缓存
  4. 等待TCP队列
  5. 建立TCP链接
  6. 发送HTTP请求

服务器端处理HTTP请求流程:

  1. 返回请求
  2. 断开链接:通常情况下一旦服务器向客户端返回了请求数据,就要关闭TCP链接,但是如果加这一行可以保持链接。
Connection: Keep-Alive
  1. 重定向:如果返回301,告诉浏览器需要重定向,地址在响应头的Location字段中。

Q&A:

  1. 浏览器可以打开多个标签,端口一样吗?如果一样,数据如何知道去哪个标签页 端口一样的,网络进程知道每个TCP链接所对应的标签是哪个,收到数据后,会把数据分给对应标签页的渲染进程。
  2. 为什么很多网站二次打开速度快? 因为第一次打开缓存了一些数据,比如DNS缓存和页面资源缓存。 50b91f9f8d61859cb1171cb6776a9cc.jpg
  3. 登录状态保持? 服务器将标识用户身份的id写到cookie中,响应头中返回
Set-Cookie: UID=xxxxxx

浏览器接收到响应头后将这个字段信息保存在本地,再次访问,浏览器读取之前保存的cookie信息发给服务器

扩展 浏览器缓存 HTTP相关1.0 2.0,HTTPS这种

03 从输入URL到页面展示发生了什么

(一)导航阶段 首先是需要各个浏览器进程之间的互相配合: 269b144dcf41a7c751db971b90e3c32.jpg

  • 浏览器接收用户输入的URL请求,浏览器将URL转发给网络进程
  • 网络进程发起URL请求(查找缓存、建立链接、重定向)
  • 网络请求收到响应数据,解析响应头,转发数据给浏览器进程
  • 浏览器进程收到响应头数据后,发送提交导航(CommitNavigation)消息到渲染进程
  • 渲染进程收到消息后准备接收HTML数据,直接和网络进程建立数据管道通信
  • 文档传输完成后,渲染进程最后向浏览器进程确认提交,“已经准备好接收和解析页面数据了”
  • 浏览器进程接收到渲染进程的消息后,移除之前旧的文档,更新浏览器进程的页面状态(安全状态、地址栏URL、history等)

其中,用户发出URL请求到页面开始解析的过程叫导航。

(二)渲染阶段

fab59c49bac2a538b22fbc780ee62bc.jpg

  • 构建DOM树:HTML经过解析,输出一个树状结构的DOM
  • 样式计算:将css属性标准化,计算出DOM树中每个节点的具体样式(涉及到css的继承和层叠规则)
  • 布局阶段:计算出DOM树中可元素的几何位置:(1)创建布局树,额外的构建一颗只有可见元素布局树 (2)布局计算
  • 分层:渲染引擎为特定的节点生成专用的图层,并生成一个图层树(Layer Tree)。并不是每一个节点都有单独的图层,拥有层叠上下文属性的元素会被提升为单独的一层,比如(明确定位属性的元素、定义透明度、使用css滤镜等,需要裁剪的地方也会有单独的一层)
  • 图层绘制:渲染引擎实现的图层绘制与绘画类似,会把一个图层的绘制拆分出很多绘制指令,再把这些指令组成一个待绘制列表。
  • 栅格化(raster):绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操纵是由渲染引擎的合成线程完成的。合成线程将图层划分为图块,按照视口附近的图块优先生成位图。实际上生成位图的操作由栅格化执行的,栅格化就是将图块转成位图,该过程由GPU来加速生成,保存在GPU内存中
  • 合成和显示:所有图块被光栅化后,合成线程会生成一个绘制图块的命令“DrawQuad”,然后将该命令教给浏览器主线程,接收合成线程发过来的命令,并将内容绘制到内存中,最终显示到界面上。

重排、重绘和直接合成

  1. 更改了元素的几何属性,位置、高度等,引起重新布局,重排 90727488c5de4d6949aacd74f7a5d25.jpg
  2. 更改元素的绘制属性 改变元素的背景颜色,布局不会重排,直接进入绘制阶段,重绘
  3. 直接合成阶段 如果更改一个既不要布局也不要重新绘制的属性,渲染引擎会跳过布局和绘制,只执行后续的合成操作,比如css的tansform【不太懂】,而且也不是在渲染进程的主线程操作,会大大提高绘制效率 313b51c1e2a11c06b35cf7b8b1c3b4f.jpg Q&A:在优化web性能的方法中,减少重排重绘的手段?

二、浏览器中js的执行机制

1 变量提升

执行以下程序:

showName()
console.log(myName)
var myName = 'xxx'
function showName() {
    console.log('函数showName被执行')
}

结果:

image.png

变量提升

js中的声明和赋值

var myName = 'xxx'
可以看成:
var myName // 声明部分
myName = 'xxx' // 赋值部分

变量提升,是指在js代码执行的过程中,js引擎把变量的声明部分和函数的声明部分提到代码开头的行为。变量提升后会给变量设置默认值,这个默认值就是undefined. 模拟变量提升:

// 变量提升部分
var myName = undefined;
function showName() { console.log(函数showName被执行)}

// 可执行代码部分
showName()
myName = 'xxx'

js代码的执行流程

实际上变量和函数声明在代码里的位置是不会改变的,而是编译阶段被js引擎存入内存中。

(1)编译阶段

一段代码经过编辑会输入执行上下文(Execution Context)和可执行代码,前者是js执行一段代码时的运行环境,比如调用一个函数就会进入这个函数的执行上下文,确定函数在执行期间用到的this 变量 对象等。

239bb8210706f55710a87fb64735c25.jpg

(2)执行阶段

js引擎开始执行可执行代码,按照顺序一行一行执行。

代码中出现相同的变量或函数?

function showName2() { console.log('1111') }
showName2()
function showName2() { console.log('222') }
showName2()

分析代码,首先执行编译阶段,第一个函数体被放到变量环境中,第二个会将第一个覆盖;执行阶段会打印出2个'222' 因此,一段代码如果定义了两个相同名字的函数,最终生效的只有一个。

Q&A:分析代码结果

showName()
var showName = function(){ console.log('aaa')}
function showName() { console.log(1) }
showName()

答案:

// 模拟过程 
// 编译部分
var showName
function showName() { console.log(1) }

// 可执行部分
showName() // 打印 1
showName = function(){ console.log('aaa')} // showName被重新赋值
showName() // 打印aaa

image.png

2 JS调用栈

在函数调用的时候,会为每个函数确定一个执行上下文,js引擎正是通过栈这种数据结构来管理执行上下文的,在执行上下文创建好后,js引擎会将执行上下文压入栈中。这种用来管理执行上下文的栈就是js的调用栈。

var a = 2;
function add(b, c) { return b + c }
function addAll(b, c) {
    var d = 10;
    var result = add(b, c);
    return a + result + d;
}
addAll(3, 6)
  • 每调用一个函数,js为其创建执行上下文,并压入调用栈,然后js引擎开始执行函数代码
  • 如果在一个函数A中调用了另一个函数B,则js引擎会为b创建执行上下文,将b函数的执行上下文压入栈顶
  • 当前函数执行完毕后,js引擎会将该函数的执行上下文弹出栈
  • 当分配的调用栈被占满时,引发“堆栈溢出”的问题

(1) 利用浏览器查看调用栈的信息

(2) 栈溢出(Stack Overflow)

3 块级作用域

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的声明周期。也就是说,作用域就是变量与函数的可访问范围。 在es6之前,es的作用域只有两种:全局作用域和函数作用域。

ES6引入了let和const关键字,使得js也拥有了块级作用域。

JS如何支持块级作用域的?

function foo(){
   var a = 1
   let b = 2
   {
     let b = 3
     var c = 4
     let d = 5
     console.log(a)
     console.log(b)
   }
   console.log(b) 
   console.log(c)
   console.log(d)
}   
foo()
  • 通过var声明的变量,在编译阶段会被放到执行上下文的变量环境中
  • 通过let const声明的变量,在编译阶段会被放在词法环境中
  • 作用域块内部的let声明的变量,会被存放在词法环境单独的区域中
  • 当执行到可执行代码的console.log(a)这行代码时,需要在词法环境中查找变量,沿着词法环境的栈从顶向下查询,如果没找到,则去环境变量里找

image.png

Q&A

分析词法环境,最终打印结果?


let myname= '极客时间'
{
  console.log(myname) 
  let myname= '极客邦'
}

答案: 【最终打印结果】:VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization 在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。

var的创建和初始化被提升,赋值不会被提升。
let的创建被提升,初始化和赋值不会被提升。
function的创建、初始化和赋值均会被提升。

4 作用域链和闭包


function bar() {
   console.log(myName)
}
function foo() {
   var myName = "极客邦"
   bar()
}
var myName = "极客时间"
foo() // 打印极客时间

作用域链

每个执行上下问的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,叫做outer,当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,这个查找链条就是作用域链。

image.png

3 V8工作原理

4 浏览器的页面循环系统

消息队列和事件循环

每个渲染进程都有一个主线程,既要处理DOM,又要计算样式处理布局,需要消息队列和事件循环系统来调度。

  • 使用单线程处理安排好的任务
  • 在线程运行过程中处理新任务,需要事件循环机制
  • 处理其他线程发送过来的任务,需要消息队列。是一种数据结构存放要执行的任务,符合队列先进先出的特点。
  • 处理其他进程发过来的任务,渲染进程有一个专门的IO线程用来接收其他进程传进来的消息。

image.png

消息队列中的任务类型:

输入事件(鼠标滚动、点击)、微任务、文件读写、websocket、js计时器等等; 除此之外,还有一些页面相关的事件,如js执行、解析、DOM、样式计算、布局计算、css动画

安全退出

当页面主线程执行完后,设置一个退出的标志变量,每次执行完任务后判断是否有退出标志。

页面单线程的缺点

  • 如何处理高优先级的任务,微任务和宏任务的机制应运而生。通常消息队列中的任务成为宏任务,每个宏任务都包含一个微任务队列;如果执行宏任务的过程,DOM有变化,就将变化添加到微任务列表中,不影响宏任务的执行,宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务。
  • 如何解决单个任务执行时间过久问题,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。

实践

image.png 图中灰色的就是一个个任务,每个任务下面还有子任务,其中的 Parse HTML 任务,是把 HTML 解析为 DOM 的任务。值得注意的是,在执行 Parse HTML 的时候,如果遇到 JavaScript 脚本,那么会暂停当前的 HTML 解析而去执行 JavaScript 脚本。

setTimeout 和 XMLHttpRequest

浏览器实现setTimeout

要执行一段异步任务,需要先将任务添加到消息队列中,但是定时器设置的回调需要在指定时间内执行,所以不能直接将回调添加到消息队列中。 在chrome中除了正常使用的消息队列外,还维护了需要延迟执行的任务列表,包括定时器和chromium内部一些需要延迟执行的任务,当通过js创建一个定时器时,渲染进程将定时器的回到任务添加到延迟队列中。 源码

使用setTimeout的注意事项

  1. 当前执行任务过就会影响定时器任务的执行
  2. 如果setTimeout存在嵌套调用,那么系统设置最短间隔时间为4毫秒
  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
  4. 延时执行时间有最大值,32bit来存储最大毫秒,超过立即执行
  5. 使用 setTimeout 设置的回调函数中的 this指向全局环境,而不是定义所在的对象, 用箭头函数解决。

var name= 1;
var MyObj = {
  name: 2,
  showName: function(){
    console.log(this.name);
  }
}
setTimeout(MyObj.showName,1000) // 1
  1. 对比于setTimeout,使用 requestAnimationFrame 不需要设置具体的时间,由系统来决定回调函数的执行时间,requestAnimationFrame 里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次,内如果页面未激活的话,requestAnimationFrame 也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销。

XMLHttpRequest

image.png

  1. 跨域问题
  2. HTTPS混合内容问题

宏任务和微任务

宏任务

  • 渲染事件(解析DOM、布局计算、绘制)
  • 用户交互事件(鼠标点击、页面滚动缩放)
  • js脚本执行事件
  • 网络请求完成、文件读写完成

为了协调任务在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部维护延迟队列和普通队列等,主线程采用一个for循环,不断地从任务队列中取出任务执行。我们把这些消息队列中的任务称为宏任务。

微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。微任务的技术有 MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。

V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。这个微任务队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务,这时候就需要使用这个微任务队列来保存这些微任务了。

微任务如何产生

第一种是调用MutationObserver监控某个DOM节点,然后通过js改变这个节点,当dom节点发生变化,就会产生dom变化记录的微任务。

第二种是使用Promise,当调用Promise.resolve或Promise.reject时,也会产生微任务

微任务队列何时被执行

当前宏任务的js快执行完成时,js引擎会检查全局执行上下文种的微任务队列,如果在执行微任务种产生了新的微任务,也会将该微任务添加到队列中。

Promise

Promise是如何解决嵌套回调的

嵌套回调的产生,主要原因是在发起任务请求会带上回调函数,下个任务在回调函数中处理。Promise主要通过以下两步解决嵌套回调。 (1)Promise实现了回调函数的延时绑定。 回调函数的延时绑定在代码上上创建一个promise对象,通过promise对象的构造函数来执行业务逻辑;创建好后,再使用.then设置回调函数;

(2)其次,需要将回调函数onResolve的返回值穿透到最外层。我们会根据onResolve函数的传入值决定创建什么类型的Promise任务,创建好的promise对象需要返回到最外层。

1d6756fef6ad471295d4c47c4e36e4a.jpg

Promise与微任务

Promise之所以需要微任务,是由回调函数延迟绑定技术导致的。new Promise的时候需要执行promise的方法,发生了先执行方法后添加回调的工程,此时需要等待then方法绑定两个回调后才能继续执行方法回调,便可将回调添加到当js调用栈中执行结束后的任务队列中,由于宏任务较多容易阻塞,则采用了微任务。

async和await

aync和awit使用了Generator和Promise两种技术。

生成器 VS 协程

生成器是一个带*号的函数,可以暂停执行和恢复。

function* genDemo() {
   console.log('开始执行第一段')
   yield 'generator 1'
   
   console.log('开始执行第二段')
   yield 'generator 2'
   
   console.log('开始执行第三段')
   yield 'generator 3'
   
   console.log('执行结束')
   return 'generator 4'
}
console.log('main 0') 
let gen = genDemo();
console.log(gen.next().value)
console.log('main 1');
console.log(gen.next().value)

// 打印结果
main 0
开始执行第一段
generator 1
main 1
始执行第二段
generator 2
  1. 在生成器内部执行一段代码,如果遇到yield关键字,js将返回关键字后面的内容给外部,并暂停该函数的执行
  2. 外部函数可以通过next方法恢复函数的执行。

协程是比线程更轻量级的存在,协程可以看作是跑在线程上的任务,一个线程上可以存在多个协程,但是线程上同时只能执行一个协程,如果当前启动的是A协程,要启动b协程,a协程就需要将主线程控制权交给b协程,那么体现在a协程暂停执行,b协程恢复执行。如果从a协程启动b协程,我们通常把a协程称为b协程的父协程。

协程切换完全由程序所控制,不像切换线程一样消耗资源。

928f797d7869c18780f624962e810d0.jpg

(1)通过生成器函数创建一个协程gen,并没有立即执行 (2)调用next函数交给gen协程 (3)协程执行时,遇到yield关键字暂停gen协程的执行,并返回信息给父协程 (4)如果协程在执行过程中遇到return关键字,js结束当前协程,return后面内容返回给父协程

850e53f494fbe1a91761ab2f6a17258.jpg

async/await

  1. async是一个异步执行并隐式返回Promise作为结果的函数
  2. await
async function foo() {
   console.log(1);
   let a = await 100;
   console.log(a)
   console.log(2)
}
console.log(0)
foo()
console.log(3)

// 0 1 3 100 2

------分割线
console.log(0)
await foo()
console.log(3)

// 0 1 100 2 3

5846cac21a6183a7a39fb1fdaca2ab1.jpg

Q&&A

async function foo() {
    console.log('foo')
}
async function bar() {
    console.log('bar start')
    await foo()
    console.log('bar end')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
    console.log('promise executor')
    resolve();
}).then(function () {
    console.log('promise then')
})
console.log('script end')

image.png

分析:

  1. 首先主协程中初始化异步函数foo和bar,遇到console打印script start
  2. 解析到setTimeout初始化一个Timer放到延时队列中
  3. 执行bar,遇到async,控制权交给协程,打印bar start,遇到await,相当于new 一个promise,执行foo,打印foo,创建一个Promise返回给主协程
  4. 返回的promise添加到微任务队列
  5. 向下执行new Promise, 输出promise executor,返回一个resolve添加到微任务队列
  6. 输出script end
  7. task结束之前检查微任务队列,按顺序执行,bar end, promise then
  8. 当前任务执行完毕后从任务队列中那拿出计时器任务,打印setTimeout

5 浏览器中的页面

5.1 chrome DevTools

Elements、Console、Sources、NetWork、Performance、Memory、Application、Security、Audits、Layers

241d26cc345eee4df36e7e2da623db4.jpg

网络面板

126e62292f1b765ebea17b3c02fa5d1.jpg

  1. 控制器

2d000466e6cea9d73e2f5d41faa883f.jpg

  1. 过滤器
  2. 抓图信息可以用来分析用户等待页面加载时间所看到的内容
  3. 时间线
  4. 相信列表
  5. 下载信息摘要,关注DOMContentLoaded和Load两个事件,以及着两个事件的完成时间。
  • DOMContentLoaded 页面以及构建好DOM
  • Load 浏览器加载了所有的资源

网络面板中的详细列表

  1. 列表的属性
  2. 详细信息
  3. 三个资源的时间线Timing

05e082b9893100d44c8d4b8efb41774.jpg

优化时间线上的耗时项

  1. 排队时间过久,域名分片技术或者HTTP2(没有每个域名最多维护6个TCP的限制)
  2. 第一字节时间过久:服务器生成页面数据时间过久;网速的原因;请求头带了多余的用户信息 3.content download太久,可能是文件过大,减少文件体积、压缩的方法。

5.2 DOM树

DOM树,就是渲染引擎能够理解的内部结构,在渲染引擎中,DOM有三个层面的作用:

  1. dom是生成页面的基础数据结构
  2. dom提供给js脚本的接口,通过这套接口,js可以对dom结构进行访问,从而改变节点的样式、文档结构等
  3. dom是一道安全防护线,不安全的内容会被过滤

DOM树的生成

渲染引擎内部,有一个HTML解析器(HTML Parser)的模块,负责将HTML字节流转成dom结构。HTML解析器是网络进程加载了多少数据就解析了多少数据。

  • 网络进程接收到content-type为text/html类型,为该请求选择或创建一个渲染进程
  • 渲染进程准备好后,网络进程和渲染进程建立共享数据的管道,渲染进程将从网络进程中读取到的数据给html parser
  • html parser解析html字节流

parse过程: 8769b73a9848124c3da3d73ec57e98b.jpg

  1. 通过分词器将字节流转换成Token。
  2. 二三阶段是同步进行的将Token解析成DOM节点,并将DOM节点添加到DOM树种。parser维护了一个Token栈结构,用于计算节点间的父子关系

JS影响DOM生成

  • 如果在html中间插入一段script标签包裹的js脚本,script标签之前,所有的解析流程还是和上文一样,解析到script标签后,渲染引擎判断这是一段脚本,html parser停止dom解析,执行标签中的这段脚本。脚本执行完成,继续解析完成。
  • 如果js代码是引入的,需要先下载这段js代码。所以说js的下载过程会阻塞dom的解析。
  • 不过,chrome对此坐了预解析操作的优化。当渲染引擎收到字节流后,会开启预解析线程,用来分析文件中包含的js css文件等,解析到有这种文件,预解析线程会提前下载这些文件
  • 如果js代码中出现操作css样式,在执行js之前,还需要等待外部的css文件加载完成,并解析生成cssom对象后才能执行js脚本。无论脚本是否操作css,都会先执行css文件,解析操作再执行js。所以说js脚本是依赖样式表的,这又是一个阻塞。

5.3 渲染流水线(pipeline):css如何影响首次加载时的白屏时间?

渲染引擎无法理解css文件内容,需将其解析成cssom,体现在dom中就是document.styleSheets有两个作用:

  • 提供给js操作样式表的能力
  • 为布局树合成提供基础的样式信息

影响页面展示的因素以及优化策略

渲染流水线影响到了首次页面的展示速度,从url请求到首次显示页面的内容:

  1. 请求发出后,到提交阶段,页面展示出来的还是之前的页面内容
  2. 提交数据之后渲染进程会创建一个空白页面,称为解析空白,并等待css和js文件加载完成,生成dom和cssom,然后合成布局树
  3. 首次渲染完成后,开始进入页面生成阶段,页面会一点点被绘制出来。

影响首屏的第一个因素是网络或服务器,第二个是白屏时间,包括解析html、下载css,js、生成csssom、执行js、生成布局树、绘制页面一系列操作。

优化策略:

  1. 尽量减少文件大小
  2. 将一些不需要再解析html阶段使用的js标记成async或defer。defer异步下载,在html解析后执行;async异步下载,下载后立即执行。
  3. 对于大的css文件,可以通过媒体查询方式,拆分成多个不同场景下用的

5.4 渲染引擎的分层和合成机制

显示图像

5.5 页面性能

5.4 虚拟DOM

5.5 渐进式网页应用(PWA)

5.6 WebComponent