浏览器渲染原理与性能优化

  • 进程:是操作系统资源分配的基本单位,进程中包含线程
  • 线程:是由进程所管理的。为了提升浏览器的稳定性和安全行,浏览器才有了多进程模型

浏览器中的五个进程

  1. 浏览器进程:负责界面显示、用户交互、子进程管理,提供存储等
  2. 渲染进程:主要作用是页面渲染、脚本执行、事件处理等。每个选项卡都由单独的渲染进程
    • 渲染进程是一个多线程,包括:
      1. GUI 渲染线程:负责渲染浏览器界面,解析 HTML、CSS、构建 DOM 树和 Render Tree,布局和绘制等。当界面需要重绘和重排时,该线程就会执行
      2. JS 引擎线程:负责解析并执行 JS 代码
      3. 事件触发线程:用来控制事件循环
      4. 定时触发器线程:setInterval 和 setTimeout(默认 4ms)所在的线程
      5. 异步 http 请求线程:用于处理请求 XMLHttpRequest
  3. 网络进程:主要处理网络资源加载(HTML、CSS,、JS 等)
  4. GPU 进程:3d 绘制,提高性能
  5. 插件进程:chrome 中安装的一些插件

扩展:进程间通信使用 IPC

从输入 URL 到浏览器显示页面发生了什么?

    1. 浏览器进程的相互调用

用户输入 url 地址(关键字 会将关键字根据默认的引擎生成地址) 开始导航 --在浏览器进程里面做 浏览器进程,会准备一个渲染进程用于渲染页面 网络进程,加载资源,最终将加载的资源交给渲染进程来处理 渲染完毕显示

    1. URL 请求过程
  1. 网络七层模型(物理层,数)网(IP)传(tcp 安全可靠 分段传输,udp 丢包) (会 表 应)http
  2. 先去查找缓存,检查缓存是否过期。有直接返回缓存的内容
  3. 看域名是否被解析过,没有则 DNS 协议解析,将域名解析成 IP 地址(DNS 基于 UDP)
  4. 请求是 https ssl 协商
  5. ip 地址来进行寻址,排队等待。最多能发送 6 个 http 请求
  6. tcp 创建连接 用于传输(三次握手)
  7. 利用 tcp 传输数据(拆分成数据包 有序)可靠 有序。服务器会按照顺序来接受
  8. http 请求(请求行 请求头 请求体)
  9. 默认不会断开 keep-alive 为了下次传输数据时,可以复用
  10. 服务器收到数据后(响应行 响应头 响应体)
  11. 服务器 返回 301 302 会进行重定向操作
  12. 服务器 304 去查询浏览器缓存进行返回

注意: 1. DOMContentLoaded:DOM 构建完成的时间 2. Load:浏览器所有资源加载完毕的时间

HTTP 发展历程

  • http/0.9 在传输过程中没有请求头和请求体,服务器响应没有返回头信息,内容采用的是 ASCII 字符流来进行传输 HTML
  • http/1.0 增加了请求头和响应头,实现了多类型数据传输
  • http/1.1 默认开启持久连接,在一个 TCP 连接上可以传输多个 HTTP 请求,采用管线化的方法(每个域最多维护 6 个持久连接)解决了队头阻塞问题(服务端需要按顺序一次处理请求)。完美支持数据分块传输(chunk transfer) ,并引入客户端 cookie 机制和安全机制等
  • http/2.0 解决网络带宽使用率低(TCP 慢启动,多个 TCP 竞争带宽,队头阻塞)采用多路复用机制(一个域名使用一个 TCP 长连接,通过二进制分帧层来实现)。头部压缩(HPACK)以及服务端推送
  • http/3.0 解决 TCP 队头阻塞问题,采用 QUIC 协议。QUIC 协议是基于 UDP 的(目前:支持和部署是最大的问题)
  • http 明文传输,在传输过程中会经历路由器、运营商等环节,数据有可能被窃取和篡改(安全问题)

渲染流程

  • 浏览器无法直接使用 HTML,需要将 HTML 转化成 DOM 树。(document)
  • 浏览器无法解析纯文本的 CSS 样式,需要对 CSS 进行解析,解析成 styleSheets。CSSOM(document.styleSeets)
  • 计算出 DOM 树中每个节点的具体样式(Attachment)
  • 创建渲染(布局)树,将 DOM 树中可见节点,添加到布局树中。并计算节点渲染到页面的坐标位置。(layout)
  • 通过布局树,进行分层 (根据定位属性、透明属性、transform 属性、clip 属性等)生产图层树
  • 将不同图层进行绘制,转交给合成线程处理。最终生产页面,并显示到浏览器上 (Painting,Display)

名称解释

  • Queueing: 在请求队列中的时间
  • Stalled: 从 TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间
  • Proxy negotiation: 与代理服务器连接进行协商所花费的时间
  • DNS Lookup: 执行 DNS 查找所花费的时间,页面上的每个不同的域都需要进行 DNS 查找
  • Initial Connection / Connecting: 建立连接所花费的时间,包括 TCP 握手/重试和协商 SSL
  • SSL: 完成 SSL 握手所花费的时间
  • Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间
  • Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间
  • Content Download: 接收响应数据所花费的时间

从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。

HTTP2 的优点

  • 解析速度快 服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。
  • 多路复用 HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。 在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。
  • 首部压缩
  • 优先级 HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。
  • 流量控制 由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制 -服务器推送 HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

其中 h2 是指 HTTP2 协议,http/1.1 则是指 HTTP1.1 协议。

服务端渲染

客服端渲染:获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。 服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。 - 优点:首屏渲染快,SEO 好 - 缺点:配置麻烦,增加了服务器的计算压力

将 CSS 放在文件头部,JavaScript 文件放在底部

因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。

使用字体图标 iconfont 代替图片图标

字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。

压缩文件

在 webpack 可以使用如下插件进行压缩:
    - JavaScript: UglifyPlugin
    - CSS: MiniCssExtractPlugin
    - HTML: HtmlWebpackPlugin
其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。

~~~sh
npm install compression-webpack-plugin --save-dev

const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
    plugins: [new CompressionPlugin()]
}
~~~

图片优化

  • 图片延迟加载 在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。 首先可以将图片这样设置,在页面不可见时图片不会加载:

    <img
      data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4"
    />
    

    等页面可见时,使用 JS 加载图片:

    const img = document.querySelector('img')
    img.src = img.dataset.src
    
  • 响应式图片

    响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。

    <picture>
      <source srcset="banner_w1000.jpg" media="(min-width: 801px)" />
      <source srcset="banner_w800.jpg" media="(max-width: 800px)" />
      <img src="banner_w800.jpg" alt="" />
    </picture>
    
    @media (min-width: 769px) {
      .bg {
        background-image: url(bg1080.jpg);
      }
    }
    @media (max-width: 768px) {
      .bg {
        background-image: url(bg768.jpg);
      }
    }
    
  • 调整图片大小

    例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。

  • 降低图片质量

    例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。

    压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

    npm install image-webpack-loader -D
    
    {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use:[
            {
            loader: 'url-loader',
            options: {
            limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
            name: utils.assetsPath('img/[name].[hash:7].[ext]')
            }
            },
            /*对图片进行压缩*/
            {
            loader: 'image-webpack-loader',
            options: {
                bypassOnDebug: true,
            }
            }
        ]
    }
    
  • 尽可能利用 CSS3 效果代替图片 有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

  • 使用 webp 格式的图片 WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。 WebP 相对于 PNG JPG 有什么优势?

减少重绘重排

- 重排
    当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。

- 重绘
    当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,**重排会导致重绘,重绘不会导致重排** 。

重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

  • 什么操作会导致重排?
  1. 添加或删除可见的 DOM 元素
  2. 元素位置改变
  3. 元素尺寸改变
  4. 内容改变
  5. 浏览器窗口尺寸改变
  • 如何减少重排重绘?
  1. 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  2. 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。