不只懂 Vue 语法:为何懒加载路由和元件会提升网页效能?

问题回答

懒加载路由或元件的意思是当访问该路由,或需要显示该元件时,才载入该路由或元件。这做法会提升网页效能,在打包时,如果某路由或元件设定了懒加载,就会独立被拆成一个 JS 档案,而非塞进 app.js 这个打包後的主要 JS 档案里,因此 app.js 主档案的大小就会被缩小。当首次载入网站时,浏览器就花更少时间载入 app.js。而且会确保在载入完成当前页面的资源後,在空余时间才开始载入其他独立拆出来的 JS 档案。因此提高了网站的效能。

以下会再作详细解说。

为何需要懒加载?

打包 Vue 专案後,会产生一些 JS 档案。如果是大型应用程序或网站,可能会有一百个以上的路由。如果网络速度稍慢,载入页面的时间可能不止几秒钟了,最後使用者通常都会直接关掉。

但是,如果使用懒加载的话,当使用者进入此路由时,浏览器才载入这个路由。而不是一载入网站时,就把所有路由全都载入。所以,首次访问网站时的等候时间就能被缩短。

示范懒加载路由的实际效果

由最基本的开始说起,当你建立一个附有 Vue router 功能的 Vue CLI 的专案後,你会发现预设有以下程序码:

router/ index.js

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

预设 /about 路由是采用懒加载的手法。现在示范自行新增一个 '/products' 的路由:

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/products',
    name: 'Products',
    component: Products
  }
]

打开 Network 检查:

  • app.js 就是整个专案所用到的 JavaScript,目前是 157KB。
  • 因为 About 页使用了懒加载,所以独立拆成一个 JS 档案。

使用 prefetch 实现懒加载

但既然 About 是懒加载,为何现在一打开首页,就马上载入 about.js?打开 about.js,会发现浏览器使用 prefetch 来载入 About。

Vue 官方文件有说明 prefetch 的意思:

<link rel="prefetch"> 是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。

所以,Vue 的确有实现懒加载,做法就是以上提到,等当前页面的档案都载入完成後,才会在背後开始载入那些之後有需要用到的档案。

你也可以打开 page source 检查原始码,看到 Vue 目前以 prefetch 方法载入 about.js

使用懒加载载入 Products

接着,现在尝试把 Products 转用懒加载。看看与以上的档案大小差别。

{
    path: '/products',
    name: 'Products',
    // webpackChunkName 是产生独立 JS 档时的档名
    component: () => import(/* webpackChunkName: "products" */ '../views/Products.vue')
}

结果:

  • app.js 现在是 142KB(之前是 157KB)。
  • 产生了一个 products.js 档案。之所以名为 products.js 是因为在 webpackChunkName 注解写上 products。

有时候,我们想要把同类的路由,在打包时可以放在同一个 JS 档案里。我们只需要把 webpackChunkName 改为同一个即可。引用官方例子:

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

此功能背後是 Webpack 本身的 Code Splitting 功能,即是把程序码分开一个个输出,也就是 经常提到 chunk / bundle 的意思。作用是让我们取得较小的档案,并更易控制优先载入哪个档案。把此概念应用到懒加载路由,就是把每个页面都分成一个 chunk 输出。

当访问 About 页时,只须载入在首页 prefetch 好的 about.js

以上所有测试都是在首页 http://localhost:8080/#/ 进行。当访问 About 页时,结果会怎样?

结果:

如果点击 All tab 来看,会发现有两个 about.js,第一个是 prefetch 载入,第二个只是 prefetch cache 而己:

点击 JS tab 会更清楚。当访问 /about 页时,其实只是载入之前已在首页 prefetch 取得的 about.js 而己:

小总结:为何懒加载路由可提升效能?

总结来说,以上测试得知,使用懒加载路由可以缩减第一次载入网站时的等待时间。使用懒加载把页面分拆为一个个独立的 JS 档,而非全都放在 app.js 这个主档案里。然後,在第一次载入网站时,浏览器在载入完当前页面後,才会使用 prefetch 来载入目前还没需要用到的 JS 档案。

那什麽是懒加载元件?

懒加载元件就是载入页面时,不会马上载入元件,而是等到需要时才载入。例如 Modal、Tooltip 等这些元素不会马上显示在页面里,而是等到使用者与网页互动时才会出现,因此适合使用懒加载。

Vue 2 使用动态载入方式

以 About 页面懒载入 Img 元件为例,当按下按钮後才显示 Img 元件。

先讲 Vue 2,做法就是使用函式载入元件。注意,以下方法无法在 Vue 3 使用,Vue 3 需要用新语法defineAsyncComponent,否则会报错。

Img.vue

<template>
    <img src="https://images.unsplash.com/photo-1633479585706-69b9f3cc34aa?ixid=MnwxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwxMHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=700&q=60" alt="">
</template>

About.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button @click="show = !show"> Show Img </button>
    <div v-if="show">
      <Img />
    </div>
  </div>
</template>
export default {
  components: {
    // 为了清晰看到档名,这里使用 webpackChunkName 指定档名
    // 以下会回传 Promise
    Img: () => import(/* webpackChunkName: "Img" */'@/components/Img.vue')
  },
  data() {
    return {
      show: false,
    }
  }
}

如果要进阶设定其他 option,例如载入时的 loading 元件、timeout 限制等,就使用物件来写:

// 自行建立 Loading 元件
import Loading from '@/components/Loading.vue'

const Img = () => ({
  component: import(/* webpackChunkName: "Img" */'@/components/Img.vue'),
  loading: Loading,
  timeout: 3000
})

export default {
  components: {
    Img
  },
  data() {
    return {
      show: false,
    }
  }
}

进入 About 页面:

按按钮显示 Img 元件:

直接载入 prefetch cache 的 Img.js 以及 disk cache 的图片。

Vue 3 新增语法: defineAsyncComponent

Vue 3 需要使用 defineAsyncComponent 来实现懒载入元件。

About.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button @click="show = !show"> Show Img </button>
    <div v-if="show">
      <Img />
    </div>
  </div>
</template>
const Img = defineAsyncComponent( () => import(/* webpackChunkName: "Img" */'@/components/Img.vue'))

export default {
  components: {
    Img
  },
  data() {
    return {
      show: false,
    }
  }
}

如果要进阶设定其他 option:

// 自行建立 Loading 元件
import Loading from '@/components/Loading.vue'
import { defineAsyncComponent } from 'vue'

const Img = defineAsyncComponent( {
  // 此属性在 Vue 2 命为 component
  // 为了清晰看到档名,这里使用 webpackChunkName 指定档名
  loader: () => import(/* webpackChunkName: "Img" */'@/components/Img.vue'),
  delay: 2000,
  timeout: 3000,
  loadingComponent: Loading
})

export default {
  components: {
    Img,
    Loading
  },
  data() {
    return {
      show: false
    }
  }
}

结果如同上面示范过的 Vue 2。

最後,Vue 3 有新增 Suspense 标签来处理懒加载元件,做法会稍有不同,但目前此语法仍在试验阶段,不建议在正式专案上使用,在此先省略不作解说。

总结

  • 建议全部路由都使用懒加载。 并看该元件的功能性质来决定是否需要使用懒加载元件。
  • app.js 是整个 Vue 应用程序打包後的核心档案,第一次载入页面时,此档案的大小会影响载入网站的速度。
  • Vue 使用 Webpack 的 code splitting 功能,把设定了懒加载的元件或路由,拆分成独立一个 JS 档案,因此不会把它打包进去主档案 app.js 里。在第一次载入网站时,这做法能缩短载入网站的时间。
  • Vue 使用 prefetch 来下载独立打包出来的 JS 档案。意思是当前页面内容已下载完成後,才会下载这些档案。之後,当使用者访问这些设了懒加载的页面或元件时,就会以 prefetch cache 的形成载入它们。
  • Vue 2 在 component 使用函式来设定懒加载元件。但此做法目前没法用在 Vue 3。Vue 3 需要使用新语法 defineAsyncComponent 来完成懒加载元件。

参考资料

How To Lazy Load Components In Vue.js!
Vue School - Lazy Loading Routes (Vue CLI Only)
Improving performance in your Vue 3 application by Lazy Loading components
A Deep Dive Into Async Components In Vue! (Should You Use This?)


<<:  EP 28: Shell Routing for TopStore App

>>:  [Day 21] Edge Impulse + BLE Sense实现唤醒词辨识(中)

Vue.js 从零开始:Slot

学完component是怎麽传递之後,看似完美,如果某天PM丢出一个需求,初步了解状况後,发现有很多...

Day 28: 拯救失足专案,在现有专案内引入KMM

Keyword: KMM in exist project KMM这麽好,但是我们专案已经开发了五年...

开源网路钓鱼框架-Gophish(下)

Gophish钓鱼的寄送信件流程如下图 New Campaigns 钓鱼事件 完成前面的设定後接着选...

Dialog 关闭後更新 Grid 资料 / 显示储存的图档 - day20

目标 承前篇 当学生资料修改或上传图档後,能够在 Grid 即时更新修正後的资料,并於点选学生展开显...

爬虫怎麽爬 从零开始的爬虫自学 DAY29 python网路爬虫开爬10-从网页爬取图片

前言 各位早安,书接上回我们介绍了如何抓取图片 URL 并储存图片,今天我们要结合之前的爬虫功能从网...