2

性能优化

前端性能的一个重要指标是页面加载时间,不仅事关用户体验,也是搜索引擎排名考虑的一个因素。

1.图片性能优化

svg

可缩放矢量图形 (SVG) 是一个基于 XML 语法的 2D 矢量图形格式。

基本语法
  <svg>  包裹并定义整个矢量图。
  <line> 创建一条直线。
  <polyline> 创建折线。
  <rect> 创建矩形。
  <ellipse> 创建圆和椭圆。
  <polygon> 创建多边形。
  <path> 通过指定点以及点和点之间的线来创建任意形状。
  <defs> 定义一个可复用的图形。初始情况下 <defs> 里面的内容是不可见的。
  <g> 将多种形状组合起来。将组合后的形状置于 <defs> 中可以让它能够被复用。
  <symbol> 类似于一个组合,但是拥有一些额外的特性。通常被置于 <defs> 标签中便于复用。
  <use> 获取在 <defs> 中定义的复用对象并在 SVG 中显示出来。
使用案例
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hand Coded SVG</title>
  <style>
    html,
    body {
      height: 100%;
      width: 100%;
      background: #e9e9e9;
    }

    body {
      margin: 0;
      text-align: center;
    }

    .grid {
      width: 750px;
      height: 500px;
      margin: 0 auto;
      padding-top: 100px;
      padding-left: 100px;
      background-image: url('grid.png');
      position: relative;
    }

    .grid::before {
      content: "";
      border-left: 1px solid #7c7cea;
      position: absolute;
      top: 0;
      left: 100px;
      width: 750px;
      height: 600px;
    }

    .grid::after {
      content: "";
      border-top: 1px solid #7c7cea;
      position: absolute;
      top: 100px;
      left: 0;
      width: 850px;
      height: 500px;
    }

    /* 无填充的,粗细 5px 的黑色线条绘制,边角平滑 */
    svg {
      stroke: #000;
      stroke-width: 5;
      stroke-linecap: round;
      stroke-linejoin: round;
      fill: none;
    }
  </style>
</head>

<body>
  <div class="grid">
    <svg>
      <line x1="10" y1="10" x2="40" y2="10" />
      <line x1="10" y1="20" x2="50" y2="20" />
      <line x1="10" y1="30" x2="40" y2="30" />
      <line x1="10" y1="40" x2="50" y2="40" />
    </svg>

    <svg>
      <ellipse cx="100" cy="100" rx="40" ry="40" />
      <path d="
        M 80 80
        L 80 120
        L 120 100
        L 80 80
      " />
    </svg>
  </div>
</body>
</html>
jpg、png、gif和webp
特点
  • jpg:有损压缩,不支持透明
  • png:无损压缩,支持透明

    • png8 - 2^8色 + 支持透明
    • png24 - 2^24色 + 不支持透明
    • png32 - 2^32色 + 支持透明
  • gif:动态图,无损压缩,支持透明
  • webp:谷歌推出,压缩程度更好,不支持透明
使用场景
  • jpg -- 大部分不需要透明图片的业务场景
  • png -- 大部分需要透明图片的业务场景
  • webp -- 安卓/chrome全部
  • gif -- 动态图业务场景
  • svg矢量图 -- 小图颜色比较丰富, 兼容性较差
  • 精灵图 -- 小图颜色比较丰富
  • 字体图标 -- 页面图标,颜色单一
  • base64 -- 用于12kb以下图片
图片优化措施
  • css雪碧图/精灵图

    • 过去将多个小图标合并到一 大图中, 现在使用较少
    • 优点:减少请求数量
    • 缺点:图片较大,加载速度变慢,如果没加载回来问题很严重
  • 图片inline

    • 以图片base64方式处理图片
    • 优点:不用发送格外请求,随着html或者css文件的加载而加载
    • 缺点:大小会变大,所以不适用于很大的图片,一般12KB以下
  • 使用矢量图

    • 使用svg标签
    • 使用iconfont解决icon问题
    • 优点:随着加载页面字体,加载好图片,请求数量较少
    • 缺点:如果页面用不上字体,需要额外的开销加载字体图标
  • 使用webp图

    • 当浏览器兼容webp时尽量使用webp图片,前端使用webp实现原理如下:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webp</title>
</head>

<body>
  <img src="./img/1.jpg.webp" class="webp" alt="img">
  <img src="./img/2.jpg.webp" class="webp" alt="img">
  <script>
    const imgs = document.querySelectorAll('.webp');

    for (let index = 0; index < imgs.length; index++) {
      const img = imgs[index];
      // 判断当前浏览器是否支持webp
      img.onerror = function () {
        // 当图片加载失败,会出错。 --> 不支持webp
        img.src = img.src.slice(0, -5);
        // 解绑事件(内存泄漏) 
        img.onerror = null;
      }
    }
  </script>
</body>
</html>

2.懒加载和预加载

懒加载

页面打开时暂不加载图片,当图片出现在可视区域内才加载。

  • 优点:减少无效的资源加载,让当前页面更流畅
  • 使用场景:电商等图片很多的业务场景
  • 工作原理: 图片进入可视区域之后请求图片资源
实现懒加载过程
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>懒加载</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    img {
      float: left;
      width: 50%;
      height: 300px;
    }
  </style>
</head>

<body>
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/1.jpg" alt="1">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/2.jpg" alt="2">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/3.jpg" alt="3">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/4.jpg" alt="4">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/5.jpg" alt="5">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/6.jpg" alt="6">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/7.jpg" alt="7">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/8.jpg" alt="8">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/9.jpg" alt="9">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/10.jpg" alt="10">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/11.jpg" alt="11">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/12.jpg" alt="12">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/13.jpg" alt="13">
  <img src="./imgs/loading.gif" class="lazy" data-src="./imgs/14.jpg" alt="14">
  <script>
    // 视口高度
    const viewHeight = document.documentElement.clientHeight;

    lazyLoad();

    function lazyLoad() {
      const imgs = document.querySelectorAll('.lazy');

      for (let index = 0; index < imgs.length; index++) {
        const img = imgs[index];

        // 判断图片是否出现在可视区域内
        // 获取图片距视口的高度
        const h = img.getBoundingClientRect().top;

        if (h < viewHeight) {
          // 出现可视区域内,加载真正图片
          img.src = img.dataset.src;
          // 移除class属性
          img.removeAttribute('class');
          img.removeAttribute('data-src');
        }
      }
    }
    document.onscroll = lazyLoad;
  </script>
</body>
</html>
预加载

页面打开时就加载(其他页面)图片,需等当前页面资源加载完毕,才进行预加载,下次访问/其他页面访问 就会读取图片缓存。

  • 优点:让其他页面更流畅,资源使用时能从缓存加载,提升用户体验
  • 使用场景:页面展示之间有依赖关系需要维护
  • 工作原理:图片等静态资源在使用之前提前请求

3.浏览器渲染引擎

主要模块

一个渲染引擎主要包括:HTML 解析器,CSS 解析器,javascript 引擎,布局 layout 模块,绘图模块

  • HTML 解析器:解释 HTML 文档的解析器,主要作用是将 HTML 文本解释成 DOM 树。
  • CSS 解析器:级联样式表的解析器,它的作用是为 DOM 中的各个元素对象计算出样式信息,为布局提供基础设施 CSSOM 树
  • Javascript 引擎:使用 Javascript 代码可以修改网页的内容,也能修改 css 的信息,javascript 引擎能够解释 javascript 代码,并通过 DOM 接口和 CSSOM 接口来修改网页内容和样式信息,从而改变渲染的结果。
  • 布局(layout):在 DOM 创建之后,Webkit 需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达这所有信息的内部表示模型
  • 绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果
以上这些模块依赖很多其他的基础模块,包括要使用到网络 存储 2D/3D 图像 音频视频解码器 和 图片解码器。
所以渲染引擎中还会包括如何使用这些依赖模块的部分。
渲染过程

浏览器渲染页面的整个过程:浏览器会从上到下解析文档。

  1. 遇见 HTML 标记,调用 HTML 解析器解析为对应的 token (一个 token 就是一个标签文本的序列化)并构建 DOM 树(就是一块内存,保存着 tokens,建立它们之间的关系)。
  2. 遇见 style/link 标记 调用 css 解析器 处理 CSS 标记并构建 CSSOM 树。
  3. 遇见 script 标记 调用 javascript 解析器 处理 script 标记,绑定事件、修改 DOM 树/CSSOM 树 等
  4. 将 DOM 与 CSSOM 合并成一个渲染树。
  5. 根据渲染树来布局,以计算每个节点的几何信息。
  6. 将各个节点绘制到屏幕上。

    需要明白,这五个步骤并不一定按顺序执行完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。
    实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM
  • 预解析

    WebKit 和 Firefox 都进行了这项优化。在执行 js 脚本时,其他线程会解析文档的其余部分,
    找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,
    从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;
    预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

    在上述的过程中,网页在加载和渲染过程中会发出“DOMContentloaded”和“onload”事件
    分别在 DOM 树构建完成之后,以及 DOM 树构建完并且网页所依赖的资源都加载完之后发生、
    因为某些资源的加载并不会阻碍 DOM 树的创建,所以这两个事假多数是不同时发生的
  • 从 DOM 树到可视化图像

    • CSS 文件被 CSS 解析器解释成内部表示结构(CSSDOM)
    • CSS 解析器工作完成之后,在 DOM 树上附加解释后的样式信息,这就是 RenderObject 树
    • RenderObject 在创建的同时,Webkit 会根据网页的结构创建 RenderLayer,同时构建一个绘图上下文
    • 根据绘图上下文生成最终的图像(这一过程需要依赖图形库)

上面介绍的是一个完整的渲染过程,但现代网页很多都是动态的,这意味着在渲染完成之后,由于网页的动画或者用户的交互,浏览器其实一直在不停地重复执行渲染过程。(重绘重排),以上的数字表示的是基本顺序,这不是严格一致的,这个过程可能重复也可能交叉。

浏览器渲染优化措施
  • link标签和css样式放在head中

    • 原因:更快发送请求,请求样式资源,更快渲染
    • style标签是异步加载(加载html时可以通过加载style样式),解析 html就会尝试布局、渲染
    • link标签是同步加载,会阻塞渲染(等link标签中的资源加载并解析完毕,才可能渲染)。
    • 使用style标签可能会出现闪屏现象,因此建议使用link标签(为了解决闪屏问题)
    • 如果样式不在首屏 / 不会一上来显示。就可以用style
  • script放在body中最下方

    • 原因: 因为js可能要操作DOM;js阻塞 html 解析,导致页面会没有结构
    • js按照从上到下,顺序执行;js没有阻塞布局,渲染;js阻塞 html 解析
    • 如果js代码没有依赖关系,在script中使用async,一般js代码放在head中,加快js解析,不阻塞html渲染 (async是异步的,让js谁先加载完毕,谁就先执行;async 既不阻塞 页面渲染 也不阻塞 html 解析 )
<script src="./a.js" async></script>
<script src="./b.js" async></script>

4.重绘重排

浏览器实际工作
  • 在渲染 DOM 的时候,浏览器所做的工作实际上是:

    • 获取 DOM 后分割为多个图层
    • 对每个图层的节点计算样式结果 (Recalculate style--样式重计算)
    • 为每个节点生成图形和位置 (Layout--重排)
    • 将每个节点绘制填充到图层位图中 (Paint--重绘)
    • 复合多个图层到页面上生成最终屏幕图像 (Composite Layers--图层重组)
重绘(Repaint)
  • 重绘是一个元素外观的改变所触发的浏览器行为,例如改变 outline、背景色等属性。
  • 浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
  • 重绘不会带来重新布局,所以并不一定伴随重排。
重排(Reflow 回流)
  • 渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排
  • "重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。
  • 但是,"重排"必然导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。
触发重绘的属性
* color            * background          * outline-color
* border-style     * background-image    * outline
* border-radius    * background-position * outline-style
* visibility       * background-repeat   * outline-width
* text-decoration  * background-size     * box-shadow
触发重排(回流)的属性
盒子模型相关属性会触发重布局    定位属性及浮动也会触发重布局:        改变节点内部文字结构也会触发重布局:
        * width                        * top                         * text-align
        * height                       * bottom                      * overflow-y
        * padding                      * left                        * font-weight
        * margin                       * right                       * overflow
        * display                      * position                    * font-family
        * border-width                 * float                       * line-height
        * border                       * clear                       * vertival-align
        * min-height                   * white-space
常见的触发重排的操作
  • Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,
  • 一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,
  • 但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。
  • 所以,下面这些动作有很大可能会是成本比较高的。

    • 当你增加、删除、修改 DOM 结点时,会导致 Reflow , Repaint。
    • 当你移动 DOM 的位置
    • 当你修改 CSS 样式的时候。
    • 当你 Resize 窗口的时候(移动端没有这个问题)
    • 当你修改网页的默认字体时。
    • 获取某些属性时(width,height...) * 注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
优化手段
  • 元素位置移动变换时尽量使用 CSS3 的 transform 来代替对 top left 等的操作
  • 将多次改变样式属性的操作合并成一次操作

    • 不要一条一条地修改 DOM 的样式,预先定义好 class,然后修改 DOM 的 className
  • 将 DOM 离线后再修改

    • 由于 display 属性为 none 的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。
    • 如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发 2 次重排。
  • 不要把某些 DOM 节点的属性值放在一个循环里当成循环的变量

    • 当你请求向浏览器请求一些 style 信息的时候,就会让浏览器刷新队列,比如:

      • offsetTop, offsetLeft, offsetWidth, offsetHeight
      • scrollTop/Left/Width/Height
      • clientTop/Left/Width/Height
      • width,height
    • 当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要刷新队列,
    • 因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
  • 动画实现过程中,启用 GPU 硬件加速

    • transform: translateZ(0)
  • 利用文档碎片
  • 不要使用 table 布局
  • 使用window.requestAnimationFrame(DOMHighResTimeStamp)

    • 告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画
    • 该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。
    • 参数:DOMHighResTimeStamp,指示 requestAnimationFrame() 开始触发回调函数的当前时间
    • 返回值:一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
    • 取消方法:

      • window.cancelAnimationFrame(requestID)
      • requestID 是先前调用 window.requestAnimationFrame()方法时返回的 ID
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>requestAnimationFrame</title>
  <style>
    #box {
      width: 100px;
      height: 100px;
      background-color: #F90;
    }
  </style>
</head>

<body>
  <div id="box"></div>
  <script>
    const box = document.getElementById('box');

    let x = 0;
    move();
    function move() {
      // 动画最流畅,性能最好
      window.requestAnimationFrame(function () {
        // 这个函数会在下一次重排重绘之前调用(将当前函数操作dom导致的重排重绘和下一次重排重绘合并成一次)
        // 执行动画
        x++;
        box.style.transform = `translateX(${x}px)`;
        if (x >= 1000) {
          return;
        }
        move();
      })
    }
  </script>
</body>
</html>

前端性能优化措施优秀文章精选

性能优化具体 35 条措施
移动H5前端性能优化

说明:笔者只是个在前端道路上默默摸索的初学者,若本文涉及错误请及时给予纠正,如果本文对您有帮助的话,请点赞收藏关注,你的认可是我前进的动力,谢谢!


捕猹少年闰土
42 声望0 粉丝