优化前

先感受下优化前编译好的 Javascript 文件及其大小:

❯ ll ../static/js/admin/
total 15M
-rw-r--r-- 1 dongwm staff 388K Aug 17 14:00 app.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:00 chunk-01433734.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:00 chunk-0b283872.js
-rw-r--r-- 1 dongwm staff 323K Aug 17 14:00 chunk-20f6014c.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:00 chunk-2d0ac00f.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:00 chunk-2d0b1605.js
-rw-r--r-- 1 dongwm staff  13K Aug 17 14:00 chunk-2d0babdf.js
-rw-r--r-- 1 dongwm staff 3.1K Aug 17 14:00 chunk-2d0cfa15.js
-rw-r--r-- 1 dongwm staff 4.5K Aug 17 12:30 chunk-2d230fe7.js
-rw-r--r-- 1 dongwm staff 2.8K Aug 17 14:00 chunk-2d2382a4.js
-rw-r--r-- 1 dongwm staff 8.0M Aug 17 14:00 chunk-378562d0.js
-rw-r--r-- 1 dongwm staff  28K Aug 17 14:00 chunk-4ee4a833.js
-rw-r--r-- 1 dongwm staff  48K Aug 17 14:00 chunk-ecbac3dc.js
-rw-r--r-- 1 dongwm staff 5.3M Aug 17 14:00 chunk-vendors.js

可以看到竟然有 8M 和 5.3M 这么大的文件!在本地开发时由于本地网络打开文件很快感受不明显,但当把这些文件部署到服务器上,能明显感觉首屏打开时间是非常慢的。

好,看到了问题,我们来优化~

减小 chunk-vendors.js 体积

挨个来解决,首先看chunk-vendors.js。博客管理后台项目是基于 Vue-CLI 3 搭建的,所以可以通过yarn build --report查看打包的内容中各库文件体积的占比 (背后用了 webpack-bundle-analyzer)。build 成功后在 dist 目录下有个 report.html 文件,你可以再浏览器打开它:

这个项目的报告是这样的:

report.html

里面会列出全部编译文件,占的区域越大说明文件越大,鼠标点到某个区域都可以看到对应区域代表文件的相关信息。chunk-vendors.js在右上角,大小仅次于chunk-378562d0.js。往区域里面看,可以看到chunk-vendors.js中主要是 element-ui,其次是 vue.runtime.esm.js,其余的文件都比较小。

想要减少 chunk-vendors.js 体积,思路有 2 个:

  1. 使用公共 CDN。最彻底的方案,把 ui 库抽离出来。当然还可以「更彻底」,把 vue、vuex、vue-router、axios 等等依赖都剥离出去。
  2. 对 element-ui 中用的组件按需引入。虽然用了 element-ui,但是只用了其中一部分常用组件,但是在编译时把全部 ui 库都引入了,所以优化思路是:只包含需要的那部分。

要注意,企业开发和开源项目是否决定用公共 CDN 是有不同的考量的:

  1. 服务的重要性。凡是服务就可能会不稳定,如果你依赖外部的 CDN 提供服务,它挂了就直接影响你的企业服务,用户体验。除了问题你没有办法只能看着等着它解决。尤其是这种非营利性的 CDN,很多开发者会比较担心它的服务质量。所以服务越重要你对里面用的东西的掌控度就要越高,在企业里面一般都要做到完全可控。
  2. 用户体验。对于我们这个项目来说,管理后台部分只有有限的几个人可以选择,不影响全部用户,且使用者是「自己人」时,访问速度并不是最重要的标准,再说精简后速度是可接受的。

在 element-ui 官网中对于 2 种方案都有说明,有兴趣的可以去了解。在这里我选择了「使用公共 CDN」:

具体做法如下:

1. 把 vue 和 element-ui 从构建中剥离出来

直接在页面上引入 js 和 css 文件即可开始使用。看一下修改后的public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <link rel="stylesheet" href="https://unpkg.com/element-ui@2.4.5/lib/theme-chalk/index.css">
    <title>blog</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but blog doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <script src="https://unpkg.com/vue@2.6.6/dist/vue.runtime.min.js"></script>
    <script src="https://unpkg.com/element-ui@2.4.5/lib/index.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

使用 unpkg.com 这个 CDN 服务中对应的 vue 和 element-ui,但是应该制定要使用的版本

2. 修改 vue.config.js 配置

在 configureWebpack 键中使用externals:

module.exports = {
  ...
  configureWebpack: {
    devtool: 'eval-source-map',
    output: {
      filename: 'static/js/admin/[name].js',
      chunkFilename: 'static/js/admin/[name].js'
    },
    externals: {  // 新加这部分
      'vue': 'Vue',
      'element-ui': 'ELEMENT'
    }
  },
  ...
}

这样在编译时就不会把这 2 个库打包进来了

3. 去掉 src/main.js 部分逻辑

把原来的对应用法去掉 (下面展示 git diff 的效果):

diff --git a/admin/src/main.js b/admin/src/main.js
index cdc8eb9..165c77e 100644
--- a/admin/src/main.js
+++ b/admin/src/main.js
@@ -4,9 +4,6 @@ import Cookies from 'js-cookie'

 import 'normalize.css/normalize.css' // A modern alternative to CSS resets

-import Element from 'element-ui'
-import 'element-ui/lib/theme-chalk/index.css'
-
 import '@/styles/index.scss' // global css

 import App from './App'
@@ -17,9 +14,7 @@ import './icons' // icon

 import * as filters from './filters' // global filters

-Vue.use(Element, {
-    size: Cookies.get('size') || 'medium', // set element-ui default size
-})
+Vue.prototype.$ELEMENT = { size: Cookies.get('size') || 'medium' };

验证效果

现在重新 build 就可以看到chunk-vendors.js文件的变化:

❯ ll dist/static/js/admin/ |grep chunk-vendors.js
-rw-r--r-- 1 dongwm staff 1.1M Aug 17 14:26 chunk-vendors.js

从 5.3M -> 1.1M,降了约 80%,而且在线上 Nginx 配置了 Gzip 压缩,文件会更小,优化完成~

另外也解释下没把 vuex、vue-router、axios 等库也拆出来。优化的起因是库体积大,而这些库都比较小,没有强烈优化的意愿。另外在我的项目经验中,拆了之后用 CDN 会让请求数会变多,非常容易让整体 CDN 服务变慢。大家可以试试,如果页面外链的资源太多反而速度更慢,还不如现在这些都压缩在一个文件中来的速度快呢。

这部分全部代码看延伸阅读链接 2

减少 chunk-378562d0.js 体积

如果你细心可能会觉得编译后的文件名字很奇怪,有很多 chunk - 开头的文件:

❯ ll dist/static/js/admin/ |grep chunk
-rw-r--r-- 1 dongwm staff 323K Aug 17 14:26 chunk-20f6014c.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:26 chunk-2d0ac00f.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:26 chunk-2d0b1605.js
-rw-r--r-- 1 dongwm staff  13K Aug 17 14:26 chunk-2d0babdf.js
-rw-r--r-- 1 dongwm staff 3.1K Aug 17 14:26 chunk-2d0cfa15.js
-rw-r--r-- 1 dongwm staff 4.5K Aug 17 14:26 chunk-2d230fe7.js
-rw-r--r-- 1 dongwm staff 2.8K Aug 17 14:26 chunk-2d2382a4.js
-rw-r--r-- 1 dongwm staff  28K Aug 17 14:26 chunk-4ee4a833.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:26 chunk-685bad3f.js
-rw-r--r-- 1 dongwm staff 8.0M Aug 17 14:26 chunk-6fdb661a.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:26 chunk-7425814b.js
-rw-r--r-- 1 dongwm staff  48K Aug 17 14:26 chunk-ecbac3dc.js
-rw-r--r-- 1 dongwm staff 1.1M Aug 17 14:26 chunk-vendors.js

为什么要这么搞呢?其实这是当时写代码时候就做过得一个优化「按需加载」,我们回忆一下 router.js 文件中的路由的写法,就拿登录页举例吧:

{
  name: 'login',
  path: '/login',
  component: () => import('@/views/login'),
  hidden: true
},

代码分离 (Code Splitting) 是 Webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。() => import('@/views/login')就是一种按需加载的写法,我最喜欢这么用。其他的代码分离方案我这里就不介绍了,大家有兴趣可以去了解。

这样用户就不用一次加载整个大文件,而是把每个页面路由对应的模块分离成不同的 chunk 文件,按需加载。

PS: 当然对于我们这个管理后台的例子,除了 CreatePost 页复杂一些,其他的都算简单,所以不用代码分离文件总体也很小,另外代码分离是每个 chunk 文件还包含了一部分重复的代码,所以不拆也可以接受。

好,说回来。其实如果你用心想一下,就知道上面那个chunk-6fdb661a.js是指的那个页面。想想那个页面逻辑最复杂?对,就是 CreatePost 页面,因为里面有 tui-editor,它用到了 highlight.js、codemirror 等等。

当然,看 report.html 文件也能看到,大家可以翻到前面看看那张截图。里面占用空间大的包含 tui-editor-Editor.js、highlight.js、codemirror.js 和 jQuery 等。

一开始我的思路也是,但是拆完之后发现,由于页面上 unpkg 网站请求太多,这些文件加载时间都很长,反而更慢了。后来又试了下「组件按需引入」,发现成本高收益很低,最终文件没怎么降下来。

其实一开始用 tui-editor 是一个无奈之举,我本来不喜欢它的理由之一正是现在它暴露出来的缺点:依赖太多,文件太大。

怎么办呢?换。改用其他 Markdown 编辑器方案!然后搜索了一下,从官网找到了一个简单的例子 (延伸阅读链接 1),把它改造了一下,够用。

我把src/components/MarkdownEditor/index.vue改成了如下:

<template>
  <div id="editor">
    <textarea v-model="value" @input="update" v-bind:style="{ height: editorHeight}" @keyup="$emit('update:value', value);"></textarea>
    <div v-html="compiledMarkdown"></div>
  </div>
</template>

<script>
// import _ from 'lodash'
import marked from 'marked'
import debounce from 'lodash/debounce'

export default {
  name: 'MarddownEditor',
  props: {
    value: {
      type: String,
      default: ''
    },
    height: {
      type: String,
      required: false,
      default: '300px'
    }
  },
  computed: {
    compiledMarkdown() {
      return marked(this.value)
    },
    editorHeight() {
      return this.height
    },
  },
  methods: {
    update() {
      debounce((e) => {
        this.value = e.target.value
      }, 300)
    }
  }
}
</script>

其中 debounce 可以直接 import 的,没有把 lodash 库都引入。这点也要注意:按需,按需,按需!

这样就换了编辑器效果了 (当然也要安装 marked 和 lodash)。和之前的 tui-editor 方案相比,是有一些缺点的:

  1. 没有菜单栏。我觉得大家都会 Markdown 语法,菜单栏本来也比较鸡肋
  2. 预览方式不一样。原来是用鼠标切换编辑和预览模式,现在是分了左右 2 栏,左面是编辑,右面是直接显示 Markdown 渲染后的效果。当然这部分花点时间也能实现原来的切换编辑 / 预览的效果,不会对编译后的文件体积产生什么影响。

当然优点也非常明显:文件很小。编译后再看一下:

❯ ll ../static/js/admin/
total 2.3M
-rw-r--r-- 1 dongwm staff 388K Aug 17 14:56 app.js
-rw-r--r-- 1 dongwm staff 323K Aug 17 14:56 chunk-20f6014c.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:56 chunk-2d0ac00f.js
-rw-r--r-- 1 dongwm staff 5.1K Aug 17 14:56 chunk-2d0b1605.js
-rw-r--r-- 1 dongwm staff  13K Aug 17 14:56 chunk-2d0babdf.js
-rw-r--r-- 1 dongwm staff 3.1K Aug 17 14:56 chunk-2d0cfa15.js
-rw-r--r-- 1 dongwm staff 4.5K Aug 17 14:56 chunk-2d230fe7.js
-rw-r--r-- 1 dongwm staff 2.8K Aug 17 14:56 chunk-2d2382a4.js
-rw-r--r-- 1 dongwm staff  28K Aug 17 14:56 chunk-4ee4a833.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:56 chunk-685bad3f.js
-rw-r--r-- 1 dongwm staff  77K Aug 17 14:56 chunk-7425814b.js
-rw-r--r-- 1 dongwm staff 259K Aug 17 14:56 chunk-a1deec84.js
-rw-r--r-- 1 dongwm staff  48K Aug 17 14:56 chunk-ecbac3dc.js
-rw-r--r-- 1 dongwm staff 1.1M Aug 17 14:56 chunk-vendors.js

现在全部文件加起来才 2.3M,比一开始的 15M 降了约 85%!

这部分全部代码看延伸阅读链接 3

减少 Chunk 文件数量

对于我们这个管理后台项目,除了 CreatePost 页复杂一些,其他的页面都算简单,前面我们看到一大堆 chunk - 前缀的 js 和 css 的文件,其中很多文件很小。大家想想,打开一个页面时要访问很多静态文件势必影响加载静态资源的时间。

之前已经说过了,这是为了做代码分离 (Code Splitting)。但是当我们优化之后,「文件多且每个文件小」就成了一个问题。怎么做呢?就是「合」。也就是修改 src/router.js 中的路由,直接 import。比如 Home,原来这么写:

{
    path: '/',
    component: Layout,
    redirect: '/home',
    children: [
        {
            path: 'home',
            component: component: () => import('@/views/home'), // 注意这句
            name: 'Home',
            meta: { title: 'Home', icon: 'dashboard', affix: true }
        }
    ]
},

现在这么写:

import Home from '@/views/home'

{
    path: '/',
    component: Layout,
    redirect: '/home',
    children: [
        {
            path: 'home',
            component: Home, // 直接写引入的模块
            name: 'Home',
            meta: { title: 'Home', icon: 'dashboard', affix: true }
        }
    ]
},

不过我只把 User/Post 相关的逻辑改成上面这样用,但是也能极大的减少了文件的数量,且总文件大小也可接受:

❯ ll dist/static/*/admin/
dist/static/css/admin/:
total 44K
-rw-r--r-- 1 dongwm staff  34K Aug 17 16:50 app.css
-rw-r--r-- 1 dongwm staff 1.5K Aug 17 16:50 chunk-45698e1a.css
-rw-r--r-- 1 dongwm staff 1.9K Aug 17 16:50 chunk-vendors.css

dist/static/js/admin/:
total 2.1M
-rw-r--r-- 1 dongwm staff 761K Aug 17 16:50 app.js
-rw-r--r-- 1 dongwm staff 4.5K Aug 17 16:50 chunk-2d230fe7.js
-rw-r--r-- 1 dongwm staff 2.8K Aug 17 16:50 chunk-2d2382a4.js
-rw-r--r-- 1 dongwm staff  28K Aug 17 16:50 chunk-45698e1a.js
-rw-r--r-- 1 dongwm staff 1.3M Aug 17 16:50 chunk-vendors.js

细心的话,可以看到文件变得更小了 (原来 2.3,现在 2.1), 这是因为 chunk 文件中会有一部分重复代码,节省文件数量也达到了减少总体积的目的。

PS: 如果希望编译时取消 Splitchunks,也就是合并 chunk-vendors.XX 到 app.XX (JS 或者 CSS),可以在 chainWebpack 里加这么一段:

...
chainWebpack: config => {
  config.optimization
    .splitChunks({
      cacheGroups: {}
    })
...

这部分全部代码看延伸阅读链接 4

好啦,就优化到这里了~

延伸阅读

  1. https://cn.vuejs.org/v2/examples/index.html
  2. https://github.com/dongweiming/lyanna/commit/d369bda6f8242b98ad5f69b0bf341e6a1a93d4ba
  3. https://github.com/dongweiming/lyanna/commit/a879d4cb2823350617b8e8f0ab9dcb68f610406e
  4. https://github.com/dongweiming/lyanna/commit/986001487ff75081620dddb4db2f3ac9fa1d5a1b