浏览器原理与实现-学习5- 网络面板/分层和合成机制/js-css优化/PWA/webcompoent

177 阅读18分钟

1.网络面板

image.png 包含:控制器、过滤器、抓图信息、时间线、详细列表,下载信息概

各页签介绍

控制器

  • 抓包
  • 搜索
  • 清缓存
  • 网络模拟

过滤器

过滤内容

抓图信息

  • 渲染每一帧的图片
  • 通过判断白屏的多少分析是否卡顿异常

时间线

HTTP、HTTPS、WebSocket 加载的状态和时间的一个关系

下载信息概要

  • DOMContentLoaded,这个事件发生后,说明页面已经构建好 DOM 了,这意味着构建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已经下载完成了。
  • Load,说明浏览器已经加载了所有的资源(图像、样式表等)。

详细列表

最核心模块,所以关键数据都在这里

列表属性

image.png

列表详情

image.png

单个资源的详情

鼠标移动到 具体行的waterfall选择,会自动弹出显示 image.png

resource scheduling
  • Queuing : 每个域名最多维护 6 个 TCP 正在连接,超过需要等待,这里表示等了多久
connection start
  • Stalled : 发起连接对TCP连接的检测消耗的时间
  • DNS Lookup dns查找
  • Initial connection : 与服务端建立tcp连接
  • SSL :ssl连接
request /response
  • Request sent : 发送数据给服务端,速度超快,不到1毫秒
  • Waiting for server(TTFB) :等待接收服务器第一个字节的数据的时间点
  • Content Download 阶段 :第一字节时间到接收到全部响应数据所用的时间

网络请求优化方案

1.Queuing过久

当首屏请求的css js 资源过多,会进入Stalled状态

  • 使用多域名,解决单域名6个限制
  • 使用http2,由于多路复用解决的限制问题

2.Waiting for server(TTFB)过久

也叫 TTFB 第一字节时间

原因

  1. 服务端后台处理逻辑过于复杂,数据库读写慢
  2. 网络卡顿
  3. 请求头信息过多,多余的cookie

解决

  1. 服务端增加缓存的方案,减少磁盘读写和cpu的运算
  2. 可以通过cdn复用资源,提高加载速度,内容压缩等
  3. 减少query 参数与 不必要的cookie传递

3.Content Download 过久

减少文件大小,使用内容压缩,删除多余注释空格,gzip

2.JS dom加载优化

DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。

html解析流程

  1. 网络进程接收到响应头之后,判断content-type 的值是“text/html”
  2. 创建一个渲染进程
  3. 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道
  4. 网络进程不断读取html 并 不断发送给渲染进程,渲染进程一直不断的实时解析html

dom解析器实现

主流程 : html文本字节流-> 分词器 -> 生成节点 -> dom

  1. html文本字节流 转化为 分词列表,为不同的tag定义不同的 StartTag 和 EndTag
  2. 通过 token 栈,不断的压入和推出同步输出树形的dom 结构(抽象语法树)

流程

<html>  
<body>  
    <h1>hello</h1>  
    <div>jason</div>  
</body>  
</html>
  1. 每压入一个tag start 标签都会创建一个dom,这里压入了 html, body, h1

image.png

  1. 当当压入 h1 end 会判断上一个是否为h1 start 满足则弹出h1 的start和end

image.png

image.png

  1. 继续重复压入 div
    image.png

  2. 最终全部退出 ,包含 html 和 body都找到匹配的end

image.png

js加载阻塞dom

  • 当包含js的内联节点时,会暂停dom解析,优先执行js
  • 当js通过引入,一样会暂停dom的解析,去发起网络请求并等待网络请求结束后再执行js

js加载阻塞dom优化方案

  1. chrome已做优化,预解析优化,当读取html文本时候,会启动预解析线程,进行提前下载js
  2. 可以使用async,defer进行异步加载
    • async 一旦加载完成,会立即执行
    • defer 需要在 DOMContentLoaded 事件之后

3.css dom的加载优化

cssom解析实现

document.styleSheets

样式计算 计算布局

css加载阻塞js执行

由于js不知道代码里是否操作了css,所以默认要处理完前面所有css的下载,才能执行js

css优化方案

  • chrome已做优化,预解析优化,当读取html文本时候,会启动预解析线程,进行提前下载css
  • 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
  • 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
  • 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。

4.分层和合成机制

概念

  • 显示器显示图像的原理
    • 60HZ,每秒固定读取 60 次前缓冲区中的图像,并显示到显示器上
  • 显卡与显示器交互
    • 显卡负责:合成新的图像,并将图像保存到后缓冲区中,当显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。一般显卡也是60HZ
  • 一帧:渲染流水线生成的每一副图片称为一帧
  • 帧率 :每1秒更新60帧,60hz 或60fps,才叫流畅

页面卡顿来源

生成帧的速度过慢,达不到60Hz

重排、重绘和合成

  • 重排
    • 会让整个渲染流水线的每个阶段都执行一遍
  • 重绘
    • 绘因为没有了重新布局的阶段,操作效率稍微高点,但是依然需要重新计算绘制信息,并触发绘制操作之后的一系列操作
  • 合成
    • 基于GPU
    • 分层+合成

合成原理

  • 那么绘制过程会生成|Paint BackGroundColor:Black | Paint Circle|这样的绘制指令列表
  • 合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的

分块

  • 合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块
  • 纹理上传
    • 计算机内存上传到 GPU 内存的操作会比较慢
    • 解决方案:chrome在首次合成图块的时候使用一个低分辨率的图片

优化方案

通过will-change: transform告诉浏览器,这个单独实现合成进程渲染,不影响主线程

box {will-change: transform, opacity;}

系统地优化页面性能

页面优化定义

让页面更快地显示和响应

页面的3个阶段

  1. 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。关闭阶段,主要是用户发出关2. 闭指令后页面所做的一些清理操作。
  2. 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。

加载阶段优化方案

优化关键资源

  • 第一个是关键资源个数
  • 第二个是关键资源大小
  • 第三个是请求关键资源需要多少个 RTT(Round Trip Time)

RTT

  • 定义:RTT 就是这里的往返时延。
  • 通常 1 个 HTTP 的数据包在 14KB 左右,所以 1 个 0.1M 的页面就需要拆分成 8 个包来传输了,也就是说需要 8 个 RTT。

优化流程计算分析

  • html资源 :尽量小于14kb
  • js 和 css资源: 由于渲染引擎有一个预解析的线程,都是并发执行,计算体积最大的即可

优化策略

  1. 如何减少关键资源的个数?
    • 一种方式是可以将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。
    • 另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 sync 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 sync 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了
  2. 如何减少关键资源的大小?
    • 可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。
  3. 如何减少关键资源 RTT 的次数?
    • 可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。 在优化实际的页面加载速度时,你可以先画出优化之前关键资源的图表,然后按照上面优化关键资源的原则去优化,优化完成之后再画出优化之后的关键资源图表。

交互阶段

一个大的原则就是让单个帧的生成速度变快。

1. 减少 JavaScript 脚本执行时间

  • 一种是将一次执行的函数分解为多个任务
    • 使得每次的执行时间不要过久。
  • 另一种是采用 Web Workers
    • 你可以把 Web Workers 当作主线程之外的一个线程,在 Web Workers 中是可以执行 JavaScript 脚本的,不过 Web Workers 中没有 DOM、CSSOM 环境,这意味着在 Web Workers 中是无法通过 JavaScript 来访问 DOM 的,所以我们可以把一些和 DOM 操作无关且耗时的任务放到 Web Workers 中去执行。

2. 避免强制同步布局

1.正常情况下的布局操作

都是在另外的任务中异步完成的

<html>
<body>
    <div id="main_div">
        <li id="time_li">xxxx</li>
        <li>yyy</li>
    </div>
 
    <p id="demo"> 强制布局 demo</p>
    <button onclick="foo()"> 添加新元素 </button>
 
    <script>
        function foo() {
            let main_div = document.getElementById("main_div")      
            let new_node = document.createElement("li")
            let textnode = document.createTextNode("mynode")
            new_node.appendChild(textnode);
            document.getElementById("main_div").appendChild(new_node);
        }
    </script>
</body>
</html>
2.强制同步布局

JavaScript 强制将计算样式和布局操作提前到当前的任务中

function foo() {
    let main_div = document.getElementById("main_div")
    let new_node = document.createElement("li")
    let textnode = document.createTextNode("mynode")
    new_node.appendChild(textnode);
    document.getElementById("main_div").appendChild(new_node);
    // 由于要获取到 offsetHeight,
    // 但是此时的 offsetHeight 还是老的数据,
    // 所以需要立即执行布局操作
    console.log(main_div.offsetHeight)
}

优化代码

function foo() {
    let main_div = document.getElementById("main_div")
    // 为了避免强制同步布局,在修改 DOM 之前查询相关值
    console.log(main_div.offsetHeight)
    let new_node = document.createElement("li")
    let textnode = document.createTextNode("mynode")
    new_node.appendChild(textnode);
    document.getElementById("main_div").appendChild(new_node);
    
}

3. 避免布局抖动

是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。

  function foo() {
    let time_li = document.getElementById("time_li")
    for (let i = 0; i < 100; i++) {
        let main_div = document.getElementById("main_div")
        let new_node = document.createElement("li")
        let textnode = document.createTextNode("mynode")
        new_node.appendChild(textnode);
        new_node.offsetHeight = time_li.offsetHeight;
        document.getElementById("main_div").appendChild(new_node);
    }
}

4. 合理利用 CSS 合成动画

will-change,这是告诉渲染引擎需要将该元素单独生成一个图层。

5. 避免频繁的垃圾回收

频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。

5.虚拟DOM和实际的DOM有何不同?

操作dom的缺点

  • 会触发重排
  • 频繁dom操作甚至会导致强制同步布局和布局抖动

什么是虚拟dom

  1. 改变内容都在虚拟dom上
  2. 当内容变化时,只是对比调整虚拟dom的内容
  3. 收集完后,再一次性提交,减少重复操作的损耗

虚拟dom运行阶段

  • 创建阶段
    • 新老虚拟dom节点和真实dom一样,一次性创建首次显示的所有dom内容。
  • 更新阶段
    • 通过老的虚拟dom和新的虚拟dom比较,只针对变化的内容,做dom操作
    • react的 diff算法
      • Stack reconciler:老算法reconciliation
      • Fiber reconciler: 新算法,执行算法的过程中出让主线程,这样就解决了 Stack reconciler 函数占用时间过久的问题

双缓存

  • 正常前端显示内容是通过dom输出
    • 通过虚拟dom多加一层
  • 在游戏开发中,视图显示通过直接读取前缓冲区显示。
    • 通过多加一层缓存区2,把复杂的运算都在缓存区2完成,再讲缓存2同步到缓冲区上
  • 能解决页面无效刷新和闪屏的问题

MVC架构

  • model -> controller > view
  • 等价于 react 中的 state + vuex+ + view
  • view就是 vm,处理所有视图显示

6.PWA

定义

Progressive Web App渐进式网页应用,它由很多技术组成的一个理念,其核心思想是渐进式。

web应用对比原生

缺点

  1. 缺少离线能力
  2. 缺少服务端主动推送给客户端能力
  3. 缺少程序统一入口

web work

  • Web Worker 其实就是在渲染进程中开启的一个新线程,它的生命周期是和页面关联的
  • 数据传输使用postmessage

service work

  • 是页面和网络之间的拦截层
    • 拦截数据
    • 缓存数据
  • 架构实现
    • Service Worker 是运行在浏览器进程中的,生命周期最长
    • 可以被多个页面访问数据
  • 支持服务端推送
  • 安全
    • 必须采用 HTTPS 协议
    • 同时支持 Web 页面默认的安全策略、储入同源策略、内容安全策略(CSP)等

manifest.json

程序一级入口

7.WebComponent

组件化的思想

  • 对内高内聚,对外低耦合
    • 对内各个元素彼此紧密结合、相互依赖,对外和其他组件的联系最少且接口简单。

前端组件化的难点

CSS 和 DOM 都是定义在全局上的,所以会互相影响

webcomponent

是一套技术的组合

  • HTML templates(HTML 模板)
  • Custom elements(自定义元素)
  • Shadow DOM(影子 DOM)

操作流程

  • 使用 template 属性来创建模板
    • 模板元素是不会被渲染到页面上
  • 我们需要创建一个 自定义的类
    • 查找模板内容;创建影子 DOM;再将模板添加到影子 DOM 上。
  • 使用 HTML 元素一样使用该元素

实现局部沙盒的效果

影子 DOM实现原理

特征

影子 DOM 中的元素对于整个网页是不可见的;影子 DOM 的 CSS 不会影响到整个网页的 CSSOM,影子 DOM 内部的 CSS 只对内部的元素起作用。

实现原理

  • 每个影子 DOM 你都可以看成是一个独立的 DOM,它有自己的样式、自己的属性,内部样式不会影响到外部样式,外部样式也不会影响到内部样式。

  • 当浏览器通过 DOM 接口去查找元素时,渲染引擎会去判断 自定义标签 属性下面的 shadow-root 元素是否是影子 DOM,如果是影子 DOM,那么就直接跳过 shadow-root 元素的查询操作。

  • 当生成布局树的时候,渲染引擎也会判断 属性下面的 shadow-root 元素是否是影子 DOM,如果是,那么在影子 DOM 内部元素的节点选择 CSS 样式的时候,会直接使用影子 DOM 内部的 CSS 属性。

8.浏览上下文组

  • 在同一域名下,通过标签打开或window.open打开同一站点(包括二级域名也可以)下的都属于 同一个浏览上下文组
  • 共用一个进程
  • 如果是新的tab页签再输入,就是独立的两个进程
  • 当在标签加入 rel=noopener,则会断开同一浏览上下文组的关系,独立两个进程
  • 站点隔离
    • 在同一个页面下,内嵌不同的iframe ,也是根据同一站点原则来划分 浏览上下文组

9.requestAnimationFrame

单消息队列队头阻塞问题

通过鼠标触发的点击任务、滚动页面任务;
通过手势触发的页面缩放任务;
通过 CSS、JavaScript 等操作触发的动画特效等任务。

解决方案

  • 对消息队列的类型做细化处理
    • 输入事件的消息队列,用来存放输入事件。2.合成任务的消息队列,用来存放合成事件。3.默认消息队列,用来保存如资源加载的事件和定时器回调等事件。4.空闲消息队列,用来存放 V8 的垃圾自动垃圾回收这一类实时性不高的事件。
  • 针对不同的类型,还要分不同的场景区分优先级

渲染不同步

显示器读取缓存图片时间,和浏览器生成的合成图时间不一致,很难确保在同一帧

后果

  • 渲染进程生成的帧速比屏幕的刷新率慢,就会显示卡顿
  • 渲染进程生成的帧速比屏幕的刷新率快,就会丢帧
  • 不同的两套系统很难做到同步

解决方案

  • 当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(vertical synchronization)给 GPU,简称 VSync。
  • 当渲染进程接收到用户交互的任务后,接下来大概率是要进行绘制合成操作,因此我们可以设置,当在执行用户交互的任务时,将合成任务的优先级调整到最高。接下来,处理完成 DOM,计算好布局和绘制,就需要将信息提交给合成线程来合成最终图片了,然后合成线程进入工作状态。
  • 现在的场景是合成线程在工作了,那么我们就可以把下个合成任务的优先级调整为最低,并将页面解析、定时器等任务优先级提升。在合成完成之后,合成线程会提交给渲染主线程提交完成合成的消息.
  • 如果当前合成操作执行的非常快,比如从用户发出消息到完成合成操作只花了 8 毫秒,因为 VSync 同步周期是 16.66(1/60)毫秒,那么这个 VSync 时钟周期内就不需要再次生成新的页面了。
  • 那么从合成结束到下个 VSync 周期内,就进入了一个空闲时间阶段,那么就可以在这段空闲时间内执行一些不那么紧急的任务,比- 如 V8 的垃圾回收,或者通过 window.requestIdleCallback() 设置的回调任务等,都会在这段空闲时间内执行。

任务饿死

有些低优先级的任务可能从头到尾都没机会执行,chrome会设置了连续执行多个高优先级后,一定会执行一个低优先级

10.性能优化- lighthouse加载阶段

监测并分析 Web 性能 (Performance);

监测并分析 PWA(Progressive Web App) 程序的性能;监测并分析 Web 应用是否采用了最佳实践策略 (Best practices);监测并分析是否实施了无障碍功能 (Accessibility),无障碍功能让一些身体有障碍的人可以方便地浏览你的 Web 应用。监测并分析 Web 应用是否采实施了 SEO 搜素引擎优化 (SEO)。

可优化项 (Opportunities)

lighthouse生成指标

  • 首次绘制 (First Paint) 网络原因加载慢
  • 首次有效绘制 (First Meaningfull Paint);
    • 等价于LCP,那么有可能是加载关键资源花的时间过久,也有可能是 JavaScript 执行过程中所花的时间过久
  • 首屏时间 (Speed Index); 与上面FMP一致 首次 CPU 空闲时间 (First CPU Idle);
    • 不需要等到页面上的所有元素都可交互,只要可以对大部分用户输入做出响应即可。要缩短首次 CPU 空闲时长,我们就需要尽可能快地加载完关键资源,尽可能快地渲染出来首屏内容,
  • 完全可交互时间 (Time to Interactive);
    • 页面的内容已经完全显示出来了,所有的 JavaScript 事件已经注册完成,页面能够对用户的交互做出快速响应,通常满足响应速度在 50 毫秒以内。如果要解决 TTI 时间过久的问题,我们可以推迟执行一些和生成页面无关的 JavaScript 工作。
  • 最大估计输入延时 (Max Potential First Input Delay)。
    • 你的 Web 页面在加载最繁忙的阶段, 窗口中响应用户输入所需的时间,为了改善该指标,我们可以使用 WebWorker 来执行一些计算,从而释放主线程。另一个有用的措施是重构 CSS 选择器,以确保它们执行较少的计算。

渲染进程的关键点

  • FP 创建空白页面的这个时间点称为 First Paint
  • FCP 当页面中绘制了第一个像素时,First Content Paint
  • FMP 首次有效绘制 比较复杂,可忽略,用LCP代替,First Meaningfull Paint
  • LCP 当首屏内容完全绘制完成时,Largest Content Paint
  • DCL DOMContentLoad 事件触发
  • L OnLoadEvent事件触发

11.性能优化- 加载阶段+交互performance

概览面板

  • FPS(页面帧速)出现红色点,证明一帧渲染时间过长,会导致卡顿
  • NET 过长,请求接口时间
  • CPU图形占用过大,证明主线程执行时间过长,会影响其他任务执行
  • HEAP内存曲线一直增高,没有降下来可能是内存泄露
  • 鼠标滚动或 “WASD”四个键来可以调整选择范围

性能面板

  • Main 渲染主线程的任务执行过程
    • 渲染流水线任务
    • JavaScript 执行
    • V8 的垃圾回收
    • 定时器设置的回调任务
  • Compositor 合成线程的任务执行过程
  • 合成线程中的Raster光栅化线程池,多条线程
  • GPU : GPU 进程主线程的任务执行过程
  • Frame显示的每一视图帧
  • Network网络请求详情
  • Timings 显示关键的FP、LCP、DOMContentLoaded、Onload节点
  • 渲染进程中的Chrome_ChildIOThread - IO线程
    • 用于收发不同进程中的消息
  • Interactions 指标
    • 用来记录用户交互操作,比如点击鼠标、输入文字等交互信息

详情面板

在性能面板点击对应的指标,详情会显示具体的信息

main详细分析

  • 一个灰色task就是一个任务,里面嵌套的是一个个过程,橙色代表方法调用
  • 蓝色标识解析html
  • 紫色代表 样式计算
  • 绿色块代表绘制

页面完成流程

导航阶段流程

  1. 页面执行pagehide、visibilitychange 和 unload 等事件
  2. Send request发送请求
  3. Receive Respone 过程接受到http响应头
  4. Recive Data已经接受到数据
  5. Finish load 表示网络请求已结束

解析html阶段

  1. 先执行页面dom事件
    • readyStateChange、DOMContentLoaded
  2. 解析js,Evalute Script
    • Evalute Script 脚本执行
    • Complie Script编译脚本
    • 程序执行,调用匿名函数anonymous
    • 调用setNewArea,同时调用document.append 触发了 节点变化
    • 再次触发ParserHTML
  3. 解析css,Reculate Style
    • 生成cssom

合成生成位图阶段

  • dom事件:readyStateChange、load、pageshow
  • 执行布局 Layout
  • 更新层树 Update LayerTree
  • 准备绘制列表了Paint
  • 根据绘制列表来生成相应图层Composite Layers
  • 合成线程Compositor的对执行记录逐个处理
  • 合成线程维护Raster 线程Rasterize Paint
  • Rasterize 线程传输数据给 GPU 线程
  • GPU 生成图像显示到显示器

参考

time.geekbang.org/column/intr…