前端性能优化(一):从HTML、JavaScript、CSS开始

276 阅读7分钟

  性能优化是个老生常谈的话题了,我也是希望通过自己的总结归纳一下自己这么久的学习成果。性能优化这个问题往大了说很复杂,因为可以涉及的面太广了,可以从很多方面入手:比如网络、框架(Vue、React等)、三驾马车(HTML、JS、CSS)等等。由于本人能力有限,如果有错的和不足的地方还希望各位大佬帮忙指正,谢谢。(此篇文章借鉴了前端工匠的文章以及前端性能优化 24 条建议(2020),再此表示感谢。)

  现在就从最基本的三驾马车HTML、JS、CSS开始说起。

浏览器渲染过程

  现在原生的HTML使用其实越来越少了,大家基本上都是使用三大框架(React、Vue、Angular)来进行操作了。对DOM的操作也越来越少。DOM是渲染引擎的东西,而JS是JS引擎的东西,操作DOM是两个模块的协作来完成的,就好像是两个岛屿中有一个高速公路,一去一来都是收费的,当次数足够多的时候开销就很难被忽略,从而引发性能问题,故而尽量减少DOM操作。

想要了解如何在HTML做优化,我们得先知道浏览器是如何渲染页面的。简单来说,分为以下几个步骤:

  1. HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。
  2. 解析CSS会产生CSS规则树(CSS Rule Tree),他和DOM结构很类似。
  3. 等到JS脚本文件加载以后,通过DOM APICSSOM API来操作DOM TreeCSS Rule Tree
  4. 解析完毕以后,浏览器引擎会通过DOM TreeCSS Tree来构造渲染树(Rendering Tree),渲染树不等于DOM Tree渲染树只包含了要显示的节点和这些节点的样式信息,如果某个节点是display:none的,那么就不会在渲染树中显示。
  5. CSSRule Tree主要是为了完成匹配并把CSS Rule附加上渲染树(Rendering Tree)上的每个Element
  6. 计算每个Element的位置,这又叫layout(布局)和reflow(回流)过程。
  7. 最后通过调用操作系统Native GUI的API绘制。

  需要注意的是,在遇到JS文件时,会停止渲染,执行JS代码。因为浏览器渲染和JS执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染DOM冲突。原本DOMCSSOM的构建是互相不影响的,但是由于JS不仅可以更改DOM还可以更改CSSOM。而不完整的CSSOM是无法使用的,JS想访问CSSOM并改变它需要先拿到完整的CSSOM,所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直到其完成了CSSOM的下载和构建。也就是说,JS要操作CSSOM的情况下,浏览器会先下载和构建CSSOM,然后再执行JS,最后构建DOM。(CSSOM => JS => DOM)

  而为了避免JS文件阻塞渲染树的构建,我们可以使用async和defer关键字。

  • 绿色线:HTML解析
  • 蓝色线:JS加载
  • 红色线:JS执行

  1. <script src=“script.js”></script>(没有defer和async):遇到JS文件就先加载JS文件然后马上执行,执行完了以后再解析HTML。
  2. <script defer src=“script.js”></script>(延迟执行):异步加载JS文件,加载完成后不会立即执行,而是等到HTML解析完了以后再执行。注意,在有多个JS文件加载时,defer是有序加载的
  3. <script async src=“script.js”></script>(异步下载):同样是异步加载JS文件,加载完了以后就会立刻执行,执行完后再继续HTML的解析。在有多个JS文件加载时,asycn是无序加载的
  • 综上所述:
    1. 浏览器的工作流程:构建DOM => 构建CSSOM => 构建渲染树 => 布局 => 绘制
    2. CSSOM会阻塞渲染,只有当CSSOM构建完毕以后才会进入下一个阶段构建渲染树;
    3. 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或者async属性的script标签时,DOM构建将暂停,如果此时又刚好浏览器尚未完成CSSOM的下载和构建,由于JS可以修改CSSOM,所以需要等CSSOM构建完毕以后再执行JS,最后才重新DOM构建。
    4. CSS该放在头部而<script>放在尾部。

回流与重绘

重绘不一定回流,但是回流一定重绘

  • 回流(reflow):当我们对DOM修改引发了几何尺寸的变化(修改宽高或者隐藏掉元素)时,浏览器需要重新计算几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。常见的回流操作:

    1. 添加/删除可见的DOM元素
    2. 元素尺寸改变---边距、填充、边框、宽高
    3. 内容变化,比如input框中输入文字
    4. 浏览器窗口尺寸改变
    5. 计算offerWidth和offsetHeight属性
    6. 设置style属性的值
  • 重绘(repaint):当我们对DOM的修改导致了样式的变化,但是没有影响到几何属性(修改颜色)时,要重新计算元素的几何属性,直接为该元素绘制新的样式。常见的重绘的属性和方法:

    1. color
    2. border-style
    3. box-shadow
    4. background、background-size、background-image...

优化方案

HTML

  • 压缩空白符,减少不必要的注释等字符;
  • 尽量不要使用<iframe>,如果要使用,可以在父页面加载完以后再给<iframe>src属性赋值,避免<iframe>加载时间过长阻塞主要页面
  • 减少不必要的div,避免深层次的嵌套。不然生成DOM树的时候会消耗更大,而且遍历树的时候也会增加开销;
  • 不要把节点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
	// 获取 offsetTop 会导致回流,因为需要去获取正确的值
	console.log(document.querySelector('.test').style.offsetTop)
}

JavaScript

  • 在判断条件特别多的时候,建议使用switch语句而不是if-else语句。
  • 使用函数节流和函数防抖来减少不必要的接口请求和方法执行。
    • 函数节流:在一个单位时间内,只有一次函数回调,多次请求也只有第一次生效
      • 应用场景:
        1.获取scroll滚动的值
      function handleClick(fn,time) {
        let timer = true
           return () => {
              if (!timer) {
                 return
              }
              timer = false
              setTimeout(() => {
                 fn.call(this, arguments)
                 timer = true
              }, time)
           }
      }
      
    • 函数防抖:在一个单位时间内,只有一次函数回调,多次请求也只有第一次生效
      • 应用场景:
        1.多次点击某个按钮时
        2.input输入内容请求数据时,例如输入搜索
      function handleClick(fn,time) {
         let timer = null
         return function() {
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
               fn.call(this, arguments)
            }, time)
         }
      }
      

CSS

  • 使用contain属性,可以局部更新样式,可以避免全局的重绘或者回流,减少大量开销;
  • 使用 visibility:hidden 替换 display:none 。前者只会引发重绘,后者会引发回流;
  • 不使用table布局,可能很小的一个小改动都会导致table的重新布局,不过现在应该都是flex布局了;
  • 随着浏览器的更新,原来讲究CSS选择符应该避免节点层级过多,选择器越短越好,尽量使用ID选择器和类选择器。现在已经不重要了,浏览器在解析的时候已经不会再有这么大的开销了。
  • 动画的速度越快,回流次数越多,可以选择使用requestAnimationFrame。
    • requestAnimationFrame 最大的优势就在于两点
      • 它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘和回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧;

      • 隐藏或不可见的元素中,不会进行回流和重绘;