跳转到内容

资源加载优化

刀刀

4/7/2025

0 字

0 分钟

首屏速度是用户体验最关键的一环,资源加载速度是影响性最大的因素。资源的加载速度 = 资源大小 + 网速。资源大小影响的方面有:

  1. 压缩。压缩可以影响资源大小,但是脚手架和工具基本已经做好了,编程上没法再做更多
  2. 一部分代码分割出来做异步加载,需要使用的时候再加载出来。这是核心思想
  3. 代码尽量精简

异步加载

prefetch 加载

假设 Page1Page2 是同步加载的,Page3 是异步加载,则会有以下的流程:

流程

无论是同步还是异步都会先创建一个 link 标签去请求加载,只不过异步的 Page3 会标记 prefetch 。没有该标记则说明是需要优先加载的,同步加载的资源都加载完毕后再去加载异步加载的资源。

通过项目来观察一下 prefetch 加载的过程。无论是组件还是路由,异步加载一般都是使用 import 方法来引入使用,代码如下:

js
components: {
  HelloWord: import("../about/xx.vue");
}
js
{
  path: 'about',
  component: () => import('@/views/about')
}

排查方式

前往开发者 network 修改网络为高速 3G,刷新页面可以发现,全部的资源都在请求,只不过 about 资源状态处于 padding 等待状态,直到同步资源加载完毕才加载。

查看源代码,其引入代码有一个 typ="prefetch" 的标识。

script 加载

假设 Page1Page2 是同步加载的,Page3 是异步加载,则会有以下的流程:

script 加载流程

一开始只请求同步加载的资源,异步状态资源它不会去加载,等到用户要跳转到 Page3 它才会去加载。

Vue Cli 创建的项目内置配置已经做了 prefetch 加载了,如果想要关闭它,需要前往 vue.config.js 文件修改,关闭后默认采用的 script 加载方式。代码示例如下:
js
module.exports = {
  chainWebpack: (config) => {
    config.plugins.delete("prefetch");
  },
};

如果在关闭 prefetch 的情况下想要部分路由使用 prefetch 加载,可以为其添加注释 webpackPrefetch,后可以添加 true (没有 false 这个选项)。或者填入数字,表示优先级。数字越大优先级越高,会优先加载(true 视为 0)。

js
{
  path: 'about',
  component: () => import(/* webpackPrefetch: 10 */ '@/views/about')
},
{
  path: 'help',
  component: () => import(/* webpackPrefetch: 100 */ '@/views/help') // 优先级最高
}

🔔 提示

这个是 webpack 提供的功能,与 vue 无关,如果你是使用 webpack 创建的 react 项目也需要这么配置。

总结

  • script
    1. 做到了真正的按需引入,用到的时候再加载,不用永不加载,充分减小带宽
    2. 最大的问题在于,切换需要等待,体验感不是很流畅
  • prefetch
    1. 充分利用使用者不占用带宽的浏览时间,切换到异步加载的页面是可能已经加载好了,用户体验好
    2. 一些本次行为不会打开的页面也会加载,一定程度上浪费带宽

优化经验

  1. 使用按需引入(函数式)的版本
  2. 在组件 mounted 阶段再引入库,或者用到这个功能时再引入
  3. 利用 prefetch 调控加载顺序

下面依次查看如何操作实现。

使用按需引入(函数式)的版本

xlsx 第三方库,有很多方法,我们只用了其中几个方法,但是全部引入他会都打包,项目体积会变大。

js
import xlsx from "xlsx";

因此可以使用按需引入的方法。方法为:

  • 如果是旧版本卸载依赖重新安装最新版本,支持按需引入。(老版本的库可能不支持该方法)
  • 采用解构的方式引入我们需要的方法即可
js
import { read, utils } from "xlsx";

在组件 mounted 阶段再引入库,或者用到这个功能时再引入

有部分功能与页面展示没太大关系,可能功能也不是第一时间使用,因此可以写在函数内,在 mounted 阶段引入。例如 jquery 库,我们只在某个方法中使用到它。因此可以写在函数内,在组件的 mounted 阶段引入。

如果是通过 import xx from 'xx' 引入的第三方库,说明它们内部是通过 export default 全局导出,因此获取到值后通过 .default 获取值。例如jquery源码是通过 export default 导出,因此 res.default 才是 jquery 方法。import 异步引入返回一个 Promise ,可以使用 .then 获取回调参数。

如果是通过 import {xx} from 'xx' 引入的第三方库,说明它们内部是通过 export 按需导出,因此直接获取值即可。前面的 xlsx 也可以使用该方法异步引入,示例代码如下:

vue
<script>
let $; // 在外层定义全部变量赋值,方便在函数内使用

export default {
  mounted() {
    this.changeTest();
  },
  methods: {
    changeTest() {
      import(/* webpackPrefetch: 1000 */ "jquery").then((res) => {
        $ = res.default;
        let a = $(".test");
        a.html("hello");
      });
    },
  },
};
</script>
vue
<script>
let utils;
let read;

export default {
  mounted() {
    this.changeXlsx();
  },
  methods: {
    changeXlsx() {
      import(/* webpackPrefetch: 10 */ "xlsx").then((res) => {
        // 这里是按需导出,因此不需要default
        read = res.read;
        utils = res.utils;
      });
    },
  },
};
</script>

题外话

这种方式虽然减少了好几秒的首屏加载时间,但是代价是用户如果一进来就想要使用对应功能则可能会需要等待一会,有利有弊

图片

骨架屏与占位图

骨架屏

主要作用于在内容加载期间显示的一种空状态预览,通常用灰色块与线条模拟布局。

效果图如下所示:

效果

实现原理主要有以下几种:

  1. 静态HTML/CSS:最简单的骨架屏可以直接通过静态的HTML结构和CSS样式来实现,设计成与目标加载内容大致相似的布局
  2. 动态生成:对于更复杂或需要根据数据动态变化的布局,可以使用JavaScript动态生成骨架屏。例如,根据数据模型预估内容结构,用相应的占位符元素填充
  3. 组件库/工具:一些现代前端框架和组件库提供了内置的骨架屏组件或插件

占位图

占位图是一张 .svg 格式的图片,这类图片尺寸更小,视觉效果更丰富,适用于图形简单的图片,在图片资源加载完毕前可用于占位。

sizes 属性

含义

在响应式网页设计中,使用 <img> 标签的 sizes 属性是一种优化图片加载、提升页面性能的关键技术。

它允许开发者为不同的屏幕尺寸指定最合适的图片资源,确保用户在任何设备上都能获得最佳的视觉体验,同时避免不必要的数据传输。 sizes 属性用于指定图片在不同布局条件下的显示宽度,与 srcset 属性配合使用。

使用方法

  • 定义srcset :在 img 标签中使用 srcset 属性,列出不同分辨率的图片资源及其对应的宽度描述符
  • 设置sizes :通过 sizes 属性指定不同视口宽度下图片应占的最大宽度。可以使用媒体条件(如 min-widthmax-width )来定义这些规则
  • 指定默认 src :为了向后兼容不支持 srcsetsizes 属性的浏览器,还需要使用 src 属性指定一个默认的图片资源

示例代码

假设有一个网站布局,在屏幕宽度小于 600px 时,图片应占满整个屏幕宽度;在屏幕宽度介于 600px 到 900px 之间时,图片应占屏幕宽度的一半;在屏幕宽度超过 900px 时,图片宽度固定为 450px。

相应的 img 标签代码如下:

html
<img src="./default.jpg" srcset:"small.jpg 500w, medium.jpg 1000w, large.jpg
1500w" sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 450px"
alt="示例图片">

工作原理

当浏览器解析到 img 标签时,它会查看设备的屏幕宽度,并与 sizes 属性中定义的条件进行匹配。

根据匹配结果,浏览器会从 srcset 中选择一个最接近所需宽度的图片资源加载。如果有多个候选,浏览器会选择最接近且稍微大一点的图片,以避免加载过小而影响图片质量的资源。

如果浏览器不支持 srcsetsizes,则会回退到 src 属性指定的图片资源。

优势

  • 性能优化:通过加载与显示尺寸最匹配的图片,减少不必要的数据传输,加快页面加载速度。
  • 用户体验: 确保在各种设备和屏幕尺寸上都能获得最佳的视觉效果。
  • 灵活性: 通过媒体查询和宽度描述符,提供了一种非常灵活的图片资源管理方式,允许精细控制图片在不同条件下的选择逻辑。

懒加载

当一个网站图片数量太多,会造成很大的性能消耗。而有时候页面还没拉到下面,且用户有可能不浏览下面的数据,此时那些图片加载的性能消耗就被浪费掉了。

图片懒加载的实现原理就是在用户页面滑动到该图片的位置时再去加载图片。要想实现该功能,需要了解如下的前置知识。

前置知识

自定义指令

vue2 中,自定义指令的创建方式为通过 directives 关键字设置自定义指令,其中分为全局注册和局部注册:

  • 局部注册

    vue
    <template>
      <div id="home">
        <input type="text" v-focus="daodao" />
      </div>
    </template>
    <script>
    export default {
      name: "home",
      directives: {
        focus: {
          //参数一:当前元素,参数二:绑定的一些相关信息,参数三:虚拟dom节点,参数四:上一个拟dom
          bind(el, binding, vNode, oldvNode) {
            el.focus(); // 元素获焦
            el.value = binding.value; // 把值赋值上去
          },
        },
      },
    };
    </script>
  • 全局注册

    js
    //注册全局指令
    Vue.directive("demo", {
      //自定义指令钩子函数
      // 当被绑定的元素插入到 DOM 中时……
      inserted: function (el) {
        // 聚焦元素
        el.focus();
      },
    });

    第二个参数是一个对象,里面包含每个生命周期钩子函数。如果只关心 bindupdate 时触发相同行为,而不关心其它的钩子。可以这样写:

    js
    Vue.directive("color-swatch", function (el, binding) {
      el.style.backgroundColor = binding.value;
    });

vue3 中,自定义指令需要这样写:

  • 局部注册

    vue
    <script setup>
    // 在模板中启用 v-focus
    const vFocus = {
      mounted: (el) => el.focus(),
    };
    </script>
    
    <template>
      <input v-focus />
    </template>
  • 全局注册

    js
    const app = createApp({});
    
    // 使 v-focus 在所有组件中都可用
    app.directive("focus", {
      /* ... */
    });

    同样的,vue3 自定义指令的参数二也支持简写形式,如果是仅仅需要在 mountedupdated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

    html
    <div v-color="color"></div>
    js
    app.directive("color", (el, binding) => {
      // 这会在 `mounted` 和 `updated` 时都调用
      el.style.color = binding.value;
    });
页面元素可视

MDN 这么描述 IntersectionObserver() 方法:

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。

当一个 IntersectionObserver 对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦 IntersectionObserver 被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

翻译成大白话来说就是,它可以判断元素是否出现在可视区域中的方法。其中它的参数中 intersectionRatio 属性小于等于 0 则代表它还未出现在可视区域内。

代码实现

实现步骤如下:

  1. 为图片绑定一个自定义指令,把是否需要懒加载的参数传过去。再通过自定义属性 data- 保存该图片的路径
  2. 在元素挂载到页面上的钩子函数中判断该图片是否需要懒加载
    • 不需要懒加载,直接获取图片路径并赋值渲染
    • 需要懒加载,把本地保存的占位图片路径赋值过去
  3. 通过 new IntersectionObserver() 方法中的 observe() 判断当前图片节点是否在可视区域,在可视区域内再获取图片的自定义属性并赋值
  4. 通过 unobserve() 取消监听图片节点
vue
<template>
  <div class="box">
    <div class="wrapper">
      <img v-lazy="{ nolazy }" :data-src="src" v-bind="$attrs" class="image" />
    </div>
  </div>
</template>

<script>
function lazyBinding(el, binding) {
  const placehold =
    "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";

  const { nolazy } = binding.value;

  if (nolazy) return (el.src = el.dataset.src || placehold);

  el.src = placehold;

  const obServer = new IntersectionObserver((entries) => {
    // 如果 intersectionRatio 为 0,则目标在视野外,
    // 我们不需要做任何事情。
    if (entries.find((v) => v.intersectionRatio)) {
      el.src = el.dataset.src || placehold;
      obServer.unobserve(el);
    }
  });
  obServer.observe(el);
}

export default {
  inheritAttrs: false,
  props: {
    lazy: {
      type: Boolean,
      default: true,
    },
    src: {
      type: String,
      default: "",
    },
  },
  computed: {
    nolazy() {
      return !this.lazy;
    },
  },
  directives: {
    lazy: {
      bind(el, binding) {
        lazyBinding(el, binding);
      },
      componentUpdated(el, binding) {
        lazyBinding(el, binding);
      },
    },
  },
};
</script>

<style scoped lang="scss">
@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background-color: rgb(168, 210, 255);
  &.show {
    animation: fade 0.5s ease-in-out;
  }
}
</style>

封装成一个子组件,方便后续父组件复用。

关键图片预加载

代码

如果页面上有视觉上非常重要的图片(如 banner 图),可以使用 <link rel="preload" as="image"> 在 HTML 头部预加载这些图片。这样可以确保当用户访问页面时,关键内容能够尽快显示。

示例代码如下:

html
<link rel="preload" href="important-image.jpg" as="image" />

好处

  1. 提高性能: 预加载可以确保关键图片在 HTML 解析过程中尽早开始下载,减少了页面完全加载所需的时间。
  2. 改善用户体验: 对于用户视觉上非常重要的图片,如页面顶部的 banner 图或关键的产品图片,预加载可以确保它们能够快速显示,减少用户的等待时间。
  3. 更好的资源管理: 预加载提供了一种机制,允许开发者更精确地控制资源的加载顺序和时机。

最佳实践

  • 优先考虑关键资源: 只预加载对用户体验影响最大的图片。过多的预加载可能会浪费带宽,尤其是对于移动用户而言。

  • 限制预加载数量: 避免同时预加载过多资源,以免占用过多的网络带宽和资源,影响到其他资源的加载。

  • 使用媒体查询优化:

    通过 media 属性,可以根据设备的屏幕大小或分辨率条件性地预加载图片,优化跨设备体验。

    html
    <link
      rel="preload"
      href="large-hero.jpg"
      as="image"
      media="(min-width: 600px)"
    />
  • 考虑浏览器兼容性: 尽管<link rel="preload">在现代浏览器中得到了广泛支持,但仍有一些旧版本浏览器不支持。为这些用户提供回退方案或保证网页也能在不预加载的情况下正常使用。

浏览器协商缓存

通过合理配置 HTTP 缓存头(如 Cache-Control),可以使浏览器缓存已加载的图片,避免在用户再次访问时重新下载,从而提高页面加载速度。

Fetch API 加载 Blob 图片

通过 Fetch API 获取图片的 Blob,并利用 URL.createObjectURL 创建一个可访问的 URL 地址,可以实现先加载一个小尺寸的图片作为预览,待页面其他内容加载完成后再替换为高分辨率的图片。

首先使用 Fetch API 请求低分辨率图片的 Blob。

使用 URL.create0bjectURL 创建临时 URL,并将其设置为图片的 src。页面其他内容加载完成后,重复上述过程加载高分辨率图片并替换之。 当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。

注意

URL.createObjectURL 方法会造成一个缓存,用户短时间内可以通过缓存多次查看下载好的图片,但是后期不需要的时候需要释放掉,避免内存泄漏。

Caches API

Caches API 允许开发者将网络请求及其响应对象存储起来,用于未来的请求。

通过预缓存关键资源,可以实现即使在离线状态下也能快速加载页面。

响应式图片处理(使用 picture 元素配合 source 元素)

除了使用 srcsetsizes 属性外,还可以使用 picture 元素配合 source 元素定义不同情境下应加载的图片资源。这种方法提供了更灵活的图片响应式处理能力,允许基于设备特性(如屏幕宽度、分辨率、网络条件)选择最合适的图片。

html
<picture>
  <source media="(min-width: 800px)" srcset="large.jpg" />
  <source media="(min-width: 450px)" srcset="medium.jpg" />
  <img src="small.jpg" alt="示例图片" />
</picture>

图片压缩和优化-如 WebP 图片格式

webp 格式的图片是针对 web 浏览器性能做了特殊优化的图片格式,与 jpgpng 等格式相比,图片效果差不多,但是解码速度以及大小尺寸会更小。

在上传图片之前,可以使用工具(如 Sharp、Image0ptim、TinyPNG 等)手动或自动进行图片压缩。

这种方法可以在不影响视觉质量的前提下减少图片文件的大小从而减少加载时间。

HEIC 图片解码(wasm WebWorker)

HEIC 是一种高效的图片格式,尤其适用于 IOS 设备。虽然它提供了比 JPEG 更好的压缩效果,但浏览器的支持度不高

通过 JavaScript 库如 heic2any 可以实现 HEIC 格式到 WebPJPEG 的转换,使其能够在网页中使用。

canvas 优化-直接使用 rgba 绘制

对性能和速度要求很高的情况下可以考虑此种方式:wasm + canvas rgba

webgl 优化(GPU 加速)

WebGL(Web Graphics Library)是一种在任何兼容的网页浏览器中使用 GPU(图形处理单元)加速渲染图形的技术。

它是一个为网页内容提供强大的 3D 绘图 API 的 JavaScript API,基于 OpenGLES 2.0,能够在不需要插件的情况下在 HTML5 <canvas> 元素上进行高性能的图形渲染。

对于前端图片优化而言,WebGL 提供了比传统的 HTML 或 CSS 方法更丰富和高效的图像处理能力,特别是在进行图像滤镜、图形变换、视觉效果等高级功能时。