vue多页面首页加载优化

7,629 阅读8分钟
原文链接: zhuanlan.zhihu.com

还是以自己的多页面博客为例,执行npm run build,将打包代码部署上线后访问项目,会发现表现很糟糕,页面会出现长时间的空白等待。通过下图可以看到,总加载时长达到将近12s,这是无法忍受的性能问题,迫切需要解决。

客户端加载示意图:

从上图中可以看到,造成加载时间过慢的元凶是vendor文件,该文件存放的是项目中所有的第三方依赖。而且从下图可以看出,这个文件同时被管理端使用,体积有1.2M。

管理端加载示意图

通过下面的打包文件结构图中可以看到,vendor主要包含的依赖有highlightmavon-editorvuevue-momentmarkedvue-routeriview等,而实际中mavon-editoriview只有在管理端会用到,highlightmarked只会在客户端用到,因此优化的一个要点就是分离第三方依赖。但是分离的时候要考虑到,即便将依赖分离后依赖文件仍然存在服务器上,而相对来说很多成熟类库会提供cdn结点,访问cdn会比访问自己的服务器要快,因此优化的第一步是对相关依赖用cdn方式引入。

引入CDN

vuevuexvue-routervue-moment采用cdn引入,此外客户端还可以引入highlight的cdn,但管理端不需要。因此,需要两份不同的模板html,配置分别如下:

tpl/index.html

tpl/admin.html

由于使用了CDN,webpack就不需要再打包这几个依赖,但是webpack会默认追踪所有依赖,因此需要配置过滤的依赖项。打开webpack.base.conf.js, 增加如下配置:

externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'vue-moment': 'VueMoment',
    'highlight.js': 'hljs',

},

其中,key是node模块名称,value是项目中对模块的引用。然后执行npm run build --report,再观察一下打包后的vendor文件,如下图,可以发现,externals中的依赖都没有被打包进来。由于少了highlight.jsvendor体积一下子缩减了一半多,只有500kb左右。

但是到这里还远远不够,vendor文件还是很大,mavon-editor就占到一半。因为mavon-editor只会在管理端用到,而marked更轻量,只会在客户端用到来解析markdown。所以还需要对vendor进一步分离。webpack为我们提供一个插件CommonsChunkPlugin,它可以收集到项目中的所有同步的依赖从而进行代码分割。

代码分割

在webpack.prod.conf.js中,修改CommonsChunkPlugin的配置,如下:

new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (

          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0 && module.resource.indexOf('mavon-editor') < 0
           && module.resource.indexOf('marked') < 0
        )
      }
}),

默认配置会把所有依赖收集进vendor,通过minChunks这个钩子函数可以自定义被收集的模块,该函数返回一个布尔值。webpack会追踪所有依赖,并调用该钩子,对于返回是true的才进行收集,因此我们可以将非共同模块从依赖中剔除,这样打包后的vendor就是客户端页面和管理端都会用到的页面,而被剔除的markedmavon-editor则会被收集到各自定义的chunk中,marked被打包进clientmavon-editor被打包进manager。如下图:

然而,这里可以看到manage这个文件还是很大,主要是包含mavon-editor,而它只需要在article路由中会用到,同样的,client中的marked也只会在detail路由中用到。那么,下一步的优化在于如何让文件按需加载。

异步加载路由

对于上面提到的问题,如果把路由引入方式改为异步引入,webpack打包的时候会单独打包异步文件作为一个新的chunk,并且在需要时才会在页面中加载。以管理端为例,修改路由引入方式为异步引入,如下:

const Catagory = () => import('../pages/Catagory');
const Avator = () => import('../pages/Avator')
const Comment = () => import('../pages/Comment')
const Detail = () => import('../pages/Detail')
const Article = () => import('../pages/Article')

采用异步路由后,再打包会发现,managerclient体积进一步减小,会对每一个异步路由多出一个打包文件,而这些文件都是动态加载的。

经过上述的一系列处理后,多页面之间的代码已经解耦,自己的依赖的大文件也成功被分离。这个时候,页面加载已经明显快了很多。但是可以看到,mavon-editor这一个依赖就有250多kb,不管它被打包到什么位置,都会影响对应页面的加载。接下来我们开启gzip压缩进一步缩小依赖的体积。

gzip压缩

一般来说,gzip的开启只要服务端去做就好,但是服务端需要先根据设置好的压缩等级去压缩文件,然后再将压缩文件返回到浏览器。而这里,webpack的CompressionWebpackPlugin插件可以帮我们生成gzip的压缩文件。可以如果提前将压缩文件存放在服务器中,服务器在处理请求的时候将节省资源的压缩时间,直接读取服务器上的.gz文件。在vue-cli2中,只需要将config/index.js中的productionGzip配置项改为true即可。

上图为打包后文件的部分截图,可以看到文件体积得到了进一步压缩, 0.41那个js文件包含mavon-editor,已经从原来的300多kb降到107kb。接下来,我们在nginx中开启gzip,如下:

gzip_static on;
    gzip_min_length  5k;
    gzip_buffers     4 16k;
    gzip_comp_level 4;
    gzip_types       text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php;
    gzip_vary on;
    keepalive_timeout  65;

其中gzip_static开启后,nginx就会读取预先压缩的gz文件,这样就可以减少每次请求进行gzip压缩的CPU资源消耗,这也就是为什么需要webpack对文件作压缩的原因。所有配置的解释可以参考Nginx Gzip模块启用和配置指令详解_nginx

缓存加速

使用nginx会默认开启缓存,对静态文件的响应加上Etag,如下图。这样浏览器二次加载的时候只要服务器的静态文件没有发生变化,就会从缓存中读取,进一步优化了页面响应速度。

预渲染

以上方案解决的问题是缩小js资源体积以便加快加载速度,这样的方案在网络良好的情况下首屏渲染的速度已经够快了,但是终归讲,渲染依赖于js加载完成后的执行逻辑,而且这样的方式不利于SEO。那么进一步提高首屏加载的方案还有两个,一个是预渲染,一个是SSR,即服务端渲染,后者的方案较为复杂,我会在以后的文章中进行分析,服务端渲染的方式相比预渲染,最主要的优势是可以动态拼接数据,作为文档的一部分返回,从而实现更友好的SEO和动态分享等功能。我的博客因为没有这些需求,并且考虑到服务器的状况(单核1GB),不想给服务器增加负载,因此不考虑SSR,将采用预渲染的方式进一步加快首屏加载。

预渲染依赖一个prerender-spa-plugin插件,首先要在webpack.prod.conf.js中引入该插件,如下:

const PrerenderSPAPlugin = require('prerender-spa-plugin')

然后,在plugins中添加以下插件配置:

new PrerenderSPAPlugin(

      path.join(__dirname, '../nginx/blog'),
      ['/'],
      {
        //在一定时间后再捕获页面信息,使得页面数据信息加载完成
          captureAfterTime: 50000,
          //忽略打包错误
          ignoreJSErrors: true,
          phantomOptions: '--web-security=false',
          maxAttempts: 10,
        }
)

经过这样配置以后,打包生成的index.html就包含了预渲染的dom结构,因此首屏渲染速度会得到更大提升。但是这里要注意一个问题,异步加载的路由打包后的chunk文件被插入了head标签中,并且带有一个async属性,如下:

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Blog - SilentPort</title>
  <link href="/static/css/client.6b5a982c3673872dbaa1a8891279d36d.css" rel="stylesheet">
  <script type="text/javascript" charset="utf-8" async="" src="/static/js/3.c925dfc72043d1d1d5ac.js"></script>
</head>

而运行时的manifest文件则位于body的底部,由于async会导致加载和渲染后续文档元素的过程和当前script脚本的加载与执行并行进行(异步),因此会导致该script脚本先于manifest执行,这会产生一个webpackJsonp is not defined错误。因此在部署之前这里需要手动将async改成defer,后者在加载后续文档元素的过程中也会和当前script脚本的加载并行进行(异步),但是当前script脚本的执行要在所有元素解析完成之后,DOMContentLoaded事件触发之前完成,这样就保证了脚本后于manifest执行。

优化到这里,部署上线,就可以体验飞一般的首屏体验了。