[译] Vue App 性能优化系列一:延迟加载和代码拆分

2,744 阅读4分钟

原文链接:vueschool.io/articles/vu…

本系列将深入研究我们在 

Vue Storefront 中使用的 Vue 性能优化技术,我们的目标是让该系列成为 Vue 应用程序性能优化的完整指南。

Webpack 是如何打包的?

本系列的大部分重点都放在如何使 js bundle 更小上,首先我们需要了解 Webpack 是如何打包的。

Webpack 打包时会创建依赖关系图,该图基于 import 链接了所有的文件。假设在 Webpack 配置中指定了 main.js 作为入口点,它将成为依赖关系图的根,该文件中导入的每个 js 模块都将成为图中的节点,同时这些节点中导入的每个模块也都将成为其节点。Webpack 使用依赖关系图来检测输出的 bundle 中应该包含哪些文件,输出的 bundle 只是一个(或多个)JavaScript 文件,其中包含依赖关系图中的所有模块。这些 bundle 本质上是整个应用程序的 JavaScript。

下图可以说明整个过程:


很明显,项目越大,初始的 JavaScript bundle 就越大。bundle 越大,用户下载和解析所需的时间就越长,用户等待的时间越长,就越有可能离开网站。根据 Google 的说法,如果页面加载时间超过3秒以上,53%的移动端用户会离开。

综上所述,更大的 bundle = 更少的用户,这可以直接转化为潜在收入的损失。

延迟加载

所以如何减少 bundle 的大小呢?答案很简单:延迟加载和代码拆分。

顾名思义,延迟加载指的是延迟加载应用程序的部分 chunks,在真正需要时才去加载;代码拆分是将应用程序拆分为这些延迟加载块的过程。

大多数情况下,用户访问网站时,并不需要立即从 JavaScript bundle 中获取所有代码。例如,我们不需要花费宝贵的资源为初次访问网站的访客加载 "My Page" 区域,或者不是每个页面都需要的 modals, tooltips 等等。

每个页面加载时都下载、解析和执行整个 bundle 是一种浪费,延迟加载使我们可以拆分 bundle 且只提供所需的部分,这样用户就不会浪费时间来加载和解析根本用不到的代码。

要查看应用实际用了多少 JavaScript 代码,可以打开 DevTools -> cmd + shift + p -> 输入 coverage -> 点击 “record”。


标记为红色的是当前路由不需要的可以延迟加载的内容,如果使用了 source map,可以点击列表中的任一文件,查看哪些部分没有调用。可以看到,即使 vuejs.org 都有很大的改进空间。

下面我们来看下,如何在自己的 Vue App 中使用延迟加载。

动态导入

通过 Webpack 动态导入可以很轻松地实现延迟加载。

如果使用以下标准方式导入 JavaScript:

// cat.js
const Cat = {
  meow: function () {
    console.log("Meowwwww!")
  }
}
export default Cat

// main.js
import Cat from './cat.js'
Cat.meow()

Cat 模块将作为依赖关系图中 main.js 的一个节点。如果只在某些情况下,如对用户交互的响应,才引入 Cat 模块,该怎么办?将这个模块与初始 bundle 绑定在一起是个坏主意,因为并非始终都需要,需要告诉应用何时下载这个代码块,这就是动态导入的用武之地,看下面例子:

// main.js
const getCat = () => import('./cat.js')
// later in the code as a response to some user interaction like click or route change
getCat()
  .then({ meow } => meow())

这里用一个返回 import 函数的函数代替之前的直接导入,该函数返回一个 Promise,Webpack 会将动态导入模块的内容打包到一个单独的文件中,然后就可以根据需要下载这个模块。

动态导入基本上隔离了要添加到依赖关系图中的节点(本例中指 Cat 模块),下面的例子更好地阐明了这种机制。

假设有一个很小的网上商店,包含4个文件:

  • main.js 
  • product.js 
  • productGallery.js 
  • category.js

// category.js
const category = {
  init () { ... }
}
export default category

// product.js
import gallery from ('./productGallery.js')

const product = {
  init () { ... }
}
export default product

// main.js
const getProduct = () => import('./product.js')
const getCategory = () => import('./category.js')

if (route === "/product") {
  getProduct()
    .then({init} => init()) // run scripts for product page
}
if (route === "/category") {
  getCategory()
    .then({init} => init()) // run scripts for category page
}

根据前面的知识, product 和 category 将会作为单独的 bundle,productGallery 由于没有使用动态导入,将与 product 打包到同个 bundle 中。换句话说,动态导入为依赖关系图创建了新的入口点。

Each color is representing separate JS bundle

延迟加载 Vue 组件

接下来,看看如何在 Vue 应用程序中使用延迟加载。

const lazyComponent = () => import('Component.vue')

非常简单。以下是延迟加载 Vue 组件的常用方式:

  • 调用 import 函数

const lazyComponent = () => import('Component.vue')
lazyComponent()

  • 按需渲染组件

<template>
  <div> 
    <lazy-component v-if='someCondition' />
  </div>
</template>

<script>
const lazyComponent = () => import('Component.vue')
export default {
  components: { lazyComponent },
  data() {
    return {
      someCondition: false
    }
  }
}

// Another syntax
export default {
  components: {
    lazyComponent: () => import('Component.vue')
  }
}
</script>

只有当 someCondition 变为 true 时,lazy-component 才会被加载。

总结

延迟加载是使 Web 应用性能更好、bundle 更小的最佳方法之一,本文介绍了如何延迟加载 Vue 组件,下节将学习如何使用异步路由拆分 Vue 代码及相关的最佳实践。