基于浏览器渲染机制谈谈前端性能优化

608 阅读3分钟

最近想系统的学习一下前端性能优化的知识, 查了很多资料, 结合自己的实际经历, 梳理了一下~

本文内容:

  1. 浏览器渲染过程
  2. 如何针对渲染过程进行优化?

CRP关键渲染路径(critical render path)

这是本章性能优化的关键概念, 我将会围绕着渲染的机制和步骤, 详细地阐述每一步的优化, 依次提高页面的渲染速度和性能.

1. 基于HTTP网络从服务器请求回来数据:

请求CSS也是按照这个流程

  • 16进制的文件流
  • 浏览器将数据解析为HTML字符串
  • 按照W3C规则识别为节点
  • 生成DOM树

2. 访问页面, 请求回来的是HTML文档, 浏览器自上而下渲染,服务器会开辟多个线程去执行

  • GUI渲染线程: 渲染页面
  • JS引擎线程: 渲染Javascript代码
  • HTTP线程: 可以开辟多个, 从服务器拉取资源数据
  • DOM监听线程

3. 页面渲染过程

CSS处理

  1. 遇到Style内嵌样式, 直接进行渲染

    • 所以当CSS代码较少的时候, 可以直接使用内嵌样式, 因为HTML拉取完毕, 则CSS同时拉取完毕, 且同时渲染
    • 但是, 代码较大的时候, 可能会影响HTML的拉取速度, 而且不利于代码维护, 此时用外链的方式会更好
  2. 遇到link的外联样式, 浏览器会创建一个HTTP线程, 去异步地请求资源文件信息, GUI线程则继续渲染

    • 但是HTTP线程是有限的, 如: 谷歌浏览器并发数为(5-7个),如果超过了HTTP请求的最大并发数, 则需要排队(HTTP请求一定是越少越好)
  3. 遇到@import, 浏览器同样会开辟HTTP线程请求资源文件信息, 但是GUI线程会被阻塞, 它必须等待资源请求完毕才能继续渲染

    • 真实项目中一定要避免使用@improt

一般会把link链接放在页面头部, 这是为了在渲染DOM的同时, 就让HTTP线程去服务器拉去CSS, 当DOM渲染完毕, CSS也拉取完了. 这样可以通过并发处理, 更有效地利用时间, 提高页面的渲染速度.

JavaScript代码处理:

应该把JavaScript代码放在页面底部, 防止其阻碍GUI渲染线程, 或者给script标签, 加上deferasync

  1. 遇到<script>标签, 会阻塞GUI渲染

    如遇到 async 属性

    • 浏览器会去请求JS资源, GUI继续渲染, 但是 一旦资源请求完毕, GUI会立马阻塞, 加载JS代码

    遇到 defer 属性

    • 浏览器请求js资源, GUI继续渲染, 等到整个DOM结构加载完毕, 再回来加载JS代码

总结: 浏览器的渲染流程

DOM加载完毕后, 假设CSS还未请求完毕, 这时是有可能会去加载JavaScript代码的

  1. 处理HTML标记, 构建DOM树

  2. 处理CSS标记, 构建CSSOM树

  3. 将DOM树和CSSOM树合成 RENDER 树

  4. 根据生成的渲染树, 计算他们在视口(viewport)内确切位置的大小, 这个计算阶段叫布局(Layout)

  5. 根据渲染树以及回流的几何信息, 得到节点的绝对像素, 对页面进行绘制(painting)

我们可以看出, 在阶段4如果对布局进行改变, 就必须向下执行阶段5, 这也是为什么, 回流一定会触发重绘, 但重绘不一定触发回流的原因

绘制阶段是分层绘制的, 我们可以通过京东网来看看分层绘制的好处.

打开流程 1. Chorme F12 => 点右边上交三个点点 => more tool => Layer

由上图我们可以看出, 这些层级实际上就是我们常说的脱离文档流, 这样做的好处就在于, 我们对这些脱离文档流的DOM元素进行操作, 它导致它所在的那个层级出发回流机制.

优化方案

  1. 避免标签的深层次嵌套, 尽可能地语义化标签
  2. 尽快将CSS代码拉取到客户端
  3. 避免JavaScript代码阻塞渲染
  4. 减少回流重绘

什么操作会引发回流 (Reflow):

  1. 页面首次渲染

  2. DOM元素的尺寸和位置发生改变

  3. 元素的内容发生改变(文本改变, 加入一张尺寸不同的图片)

  4. 增加类名

  5. 操作DOM(添加或者删除可见的DOM)

  6. 获取DOM元素的视口位置宽度长度等属性(clientWidth、clientHeight、clientTop、clientLeft offsetWidth)

总结: 很明显对于会影响DOM元素大小,结构的操作,还有获取DOM元素大小信息的操作都会出发回流.(为了获取最准确的信息, 浏览器会回流一次, 以保证信息的准确性)


对于第四点,我们就有优化的思路了, 核心在于: "尽可能地减少浏览器触发回流"

我们能做的

第1点: 页面首次渲染(这个莫得办法)

第2点: 我们要尽可能让所有类似操作在一次回流中完成

第3, 4点:

  • 对于样式的改变, 也要尽可能地集中完成如:
[DOM].style.cssText = "width:300px;height:200px;border:1px solid red;"

.box {
  width:300px;
  height:200px;
  border:1px solid red;
}
[DOM].style.className = 'box'

第5点:

  • 放弃传统操作DOM的时代, 基于Vue/React通过数据影响视图
  • 元素批量修改(文档碎片)
let box = document.querySelector('#box')
let frag = document.createDocumentFragment()
for(leti = 0; i < 10; i++){
	let span = document.createElement('span')
    frag.append(span)
}
box.append(frag)

第6点:

  • 缓存我们获取的样式信息
let boxWidth = box.offsetLeft // 保存起来, 避免每次用都重新获取

其他:

  1. 具有动画效果的DOM元素,要脱离文档流(absolute/fiex)

  2. CSS硬件加速(GPU)

    • transform/filter/opacity: 使用这些属性可以让元素脱离出一个单独的层, 并进行硬件加速, 如transfrom做动画, 它比用left等属性的性能更好, 既有硬件加速, 又不会导致回流
    • 或者给元素添加一个transform: translateX(0) 同样的能够欺骗浏览器对某一个元素采取硬件加速, 但显然这样滥用内存不是一个好的做法.
  3. 避免使用table布局和CSS的JavaScript表达式

浏览器自动优化策略

对于浏览器来说, 经常每执行一行代码, 就要做一次回流, 是非常耗能的事情, 所以一般都是把所有的操作放到一个队列中, 集中渲染

渲染队列
刷新渲染队列条件:

  1. 无修改样式代码
  2. 遇到获取元素样式代码

遇到上述两种情况, 渲染队列就会立即触发回流机制.

代码层面

webpack层面优化

用我以前的项目来做一下优化~

  1. CDN 引入
// vue.config.js
  configureWebpack: {
    externals: {
      vue: 'Vue',
      'vue-router': 'VueRouter',
    },
  }
 // index.html
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script src="//cdn.bootcss.com/vue-router/2.5.3/vue-router.min.js"></script>
  1. 路由懒加载
const categoryEdit = () => import('@/views/CategoryEdit')

 path: '/categories/create', component: categoryEdit

优化前:

优化后:

其中最大的几个模块, 我都用CDN引入, 不得不说效果真的震惊..

感谢😘


如果觉得文章内容对你有帮助: