记一次 VUE 项目优化实践

爱康体检宝 PC(www.tijianbao.com/) 算是一个“老”项目,为何说“老”呢,由于在前端技术突飞猛进,天天都有新知识、新概念,甚至新框架的今天,它仍是基于vue-cli 2.x、webpack 3.x构建,显然有些老了。其次,在早期开始这个项目的时候,因为仓促上线,也没有过多的考虑性能及加载问题,目前网站上使用的图片未通过裁切,全部的库都打包到一个 vendor 里,首屏加载时间太长,等等这些问题使网站的用户体验不是太好,基于各方面的缘由,决定对它进行一次优化,主要从如下几个方面进行:javascript

  • 使用 https 及 升级成 http/2 协议
  • 合理控制缓存
  • 合理使用图片
  • 升级至 webpack4
  • 优化首屏加载
  • 优化先后的数据对比

下面分别展开来讲php

使用 https 及 升级成 http/2 协议

https 主要带来安全性方面的提高,并且 http/2 依赖于 https,只有使用 https 协议的站点能够升级 http/2 协议。css

http/2 带来了一系列的改动和优化,主要以下:html

  • 每一个服务器只用一个链接。HTTP/2 对每一个服务器只使用一个链接,而不是每一个文件一个链接。这样,就省掉了屡次创建链接的时间,这个时间对 TLS 尤为明显,由于 TLS 链接费时间。
  • 加速 TLS 交付。HTTP/2 只需一次耗时的 TLS 握手,而且经过一个链接上的多路利用实现最佳性能。HTTP/2 还会压缩首部数据,省掉 HTTP/1.x 时代所需的一些优化工做,好比拼接文件,从而提升缓存利用率。
  • 简化 Web 应用。使用 HTTP/2 可让 Web 开发者省不少事,由于不用再作那些针对 HTTP/1.x 的优化工做了。
  • 适合内容混杂的页面。HTTP/2 特别适合混合了 HTML、CSS、JavaScript、图片和有限多媒体的传统页面。浏览器能够优先安排那些重要的文件请求,让页面的关键部分先出现,快出现。
  • 更安全。经过减小 TLS 的性能损失,可让更多应用使用 TLS,从而让用户信息更安全。

这里有一篇来自 google 的 HTTP/2 简介 更为全面和权威。前端

配置主要是在编译 nginx 时加上 with-http_ssl_module 模块和 with-http_v2_module模块vue

./configure --with-http_v2_module --with-http_ssl_module
复制代码

配置服务器 conf 文件java

server {
 listen 443 ssl http2 default_server;

 ssl_certificate server.crt;
 ssl_certificate_key server.key;
 ...
}
复制代码

而后重启服务器,完成升级~webpack

合理控制缓存

缓存对于 web 应用程序相当重要,合理控制缓存能够有效提高 web 性能,咱们以前有些域下未作明确的缓存管理,虽然浏览器有默认的缓存机制,可是因为默认的机制未必能知足咱们的要求,并且各浏览器的默认机制不一样,可能形成 web 程序的表现也不一样,因此颇有必要对各资源的缓存进行精细控制。nginx

关于浏览器缓存,我写过 一篇文章 作了详细介绍,这里只说具体的实施细节:web

  • 开启 gzip (服务器优化的一总分,不属于缓存范畴)
  • 开启 etag
  • 对 html 类型的文件设置过时时间为 80s
  • 对 api 请求设置无缓存
  • 对静态资源设置一个较长时间的缓存

具体 nginx 配置以下:

# 配置 gzip
        gzip on;
        gzip_min_length 0k;
        gzip_comp_level 1;
        gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
        gzip_vary on;
        gzip_disable "MSIE [1-6]\.";

# 开启 etag
        etag on;

# 不缓存接口
        location ~* \.(?:manifest|appcache|xml|json)$ {
                add_header Cache-Control "no-cache";
        }
        
# 设置 html 过时时间为 80s
        location ~* \.(?:html)$ {
                add_header Cache-Control "max-age=80";
        }

# 给静态资源设置一个长期缓存
        location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
                expires 1M;
                access_log off;
                add_header Cache-Control "public";
        }
        # CSS and Javascript
        location ~* \.(?:css|js)$ {
                expires 1y;
                access_log off;
                add_header Cache-Control "public";
        }
复制代码

由于 html 类型的页面文件可能实时发版,不能设置较长时间的缓存,不然可能形成发版后不更新的现象。

静态资源都设置成较长缓存时间,因为发版后,若是静态资源有更新,都会产生新的 hash 值,从而使老的资源过时。

api 请求因为要实现获请最新内容,因此不设置浏览器缓存,指望的结果是能经过 etag 优化传输,但在实践中经过 nginx 设置 api 无效,具体过程还在探索中。

经过以上方式,对 web 程序的缓存进行了较精细的控制。

合理使用图片

网站自己使用的图片有两个来源,分别是 又拍云 和 咱们本身的 idc(经过阿里云加速),在使用的过程当中都没有裁切,致使总体加载的图片资源比较大,作了以下优化:

针对来自又拍云的图片资源

又拍云提供了动态裁切的功能,能够特别方便的控制图片资源。

例如对于某个图片资源,咱们须要的是一个宽为 200px 的图片,之前咱们是直接引用这个资源:domain.url/images/001.jpg ,这是一个原始资源,多是一个很大的图片,直接引用会形成浪费

经过自动裁切,咱们能够引用指定宽度的图片:domain.url/images/001.jpg!/fw/200 ,经过在 url 后加 !/fw/200,能够引用宽度为 200px 的图片,最大限度上节省资源

对于支持 webp 的浏览器,还可让其输出 webp 版本:domain.url/images/001.jpg!/fw/200/format/webp ,经过关键字 /format/webp 指定输出的资源格式为 webp

总的来讲,又拍云提供方便灵活的资源控制方法,更多细节见官方文档:help.upyun.com/knowledge-b…

针对来自 idc 的图片资源

来自 idc 的图片相对较难处理,因为没有云存储提供的功能,idc 只是单纯的作为文件存储服务器,因此没有办法动态裁切。

不过天无绝人之路,能够经过 nginx 的一个模块来实现相似的功能,这个模块就是:ngx_http_image_filter_module,经过这个模块能够对指定的资源按条件进行裁切,当 CDN 回源的时候给他裁切好的图片就能够了,部分实现了云存储的功能,具体实施以下:

1、编译 nginx 时加上 --with-http_image_filter_module

2、配置 nginx

location ~* /images/(.+)$ {
        set $width -; #图片宽度默认值
        set $height -; #图片高度默认值
        if ($arg_width != "") {
            set $width $arg_width;
        }
        if ($arg_height != "") {
            set $height $arg_height;
        }
        #image_filter_jpeg_quality 85;
        image_filter resize $width $height; #设置图片宽高
        image_filter_buffer 10M;   #设置Nginx读取图片的最大buffer。
        image_filter_interlace on; #是否开启图片图像隔行扫描
        if ($arg_info = "yes") {
        # image_filter size;
        }
        error_page 415 = 415.png; #图片处理错误提示图,例如缩放参数不是数字
    }
复制代码

经过以上配置,当咱们想访问某个资源时可能经过:domain.url/images/002.jpg?width=200 获得宽为 200px 的图片。

因为大规模部署,运维须要作更详细的测试,因此当此次优化上线时,这个功能尚未上线。当测试完成后,运维就会将这个功能部署到线上。

这个模块的官方文档是:nginx.org/en/docs/htt…

使用图片懒加载 和 webp

使用图片懒加载 主要依赖 Vue-Lazyload 这个 npm 模块,具体使用方法见:www.npmjs.com/package/vue…

这里主要说一下其中的两个功能 progressive 和 webp:

Vue.use(VueLazyload,{
  observer: true,
  attempt: 10,
  filter: {
    progressive (listener, options) {
      const is_upyun_CDN = /upyunimages\./
      if (is_upyun_CDN.test(listener.src)) {
        listener.el.setAttribute('lazy-progressive', 'true')
        listener.loading = listener.src.replace(/fw.+/, 'fw/10')
      }
    },
    webp (listener, options) {
      if (!options.supportWebp) return
      const is_upyun_CDN = /upyunimages\./
      if (is_upyun_CDN.test(listener.src)) {
        listener.src += '/format/webp'
      }
    }
  }
})
复制代码

经过 filter 总体对全部懒加载资源进行过滤控制:

progressive:容许在加载大图见,先加载一个小图,会有一个很好的用户体验

webp: 对支持 webp 的浏览器,加载资源的 webp 版本,有效下降文件大小

经过这一系列操做,能够更进一种下降没必要要的资源加载量。

升级至 webpack4

按说 webpack 3 用的好好的,为何要升级到 4 呢,缘由仍是由于新版本给咱们带来了诸多好片,并且目前已是稳定版本,主要有如下内容:

  • 更快的编译速度,网上有说提升了 98%,我虽然没有验证数据,但直观上快了,并且快了不少
  • 零配置模块打包,虽然没办法作到“零” ,可是更多更合理的默认配置,使工程构建更加方便
  • 抛弃了 CommonChunksPlugin,使用更为先进的 SplitChunksPlugin 提取公共资源
  • 使用 Tree Shaking ,有效减小业务代码体积
  • 引入 mode 属性,能够定义为 development 和 production,不用再为生产和开发环境编写过多的配置程序
  • 等等其余未说起的 以及 默认的优化

升级过程当中可能会遇到种种问题,好在有个 官方升级指南 能够帮咱们覆盖掉一部分,但这会指南过于简明扼要,具体到项目中还会有不少坑,好在经过错误提示结合伟大的google,最终都能找到答案(若是你用 baidu ,颇有可能最终爬不上来 :< ...),也能够结合网上其余一些升级方面的文章,都会颇有帮助,我这里就不细述了。

总在来讲升级 wp 4 会花一些时间,但带来的效率和性能提高绝对值得。

优化首屏加载

对于 web 单页面应用而言,一个很是大的痛点就是在首次加载时加载的资源量过大,致使用户在第一次访问时出如今白屏时间较长,如何优化这个体验是总体优化中的重中之重,因此放在最后来讲。

前面已经说了,经过减小图片大小、升级 webpack 四、优化公共资源包等等手段,都是为了这个服务(固然了,也不全是:>),这些手段都是对资源进行操做。当对这全部的资源进行了操做,如何合理处理这些资源,就到了浏览器的渲染机制,如何经过优化渲染过程,提升首屏渲染速度,是咱们下一步要考虑的。

浏览器渲染页面的过程,主要分为五步(略过请求部分,只讨论请求到资源后浏览器如何处理):

  • 处理 HTML 标记并构建 DOM 树。
  • 处理 CSS 标记并构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个渲染树。
  • 根据渲染树来布局,以计算每一个节点的几何信息。
  • 将各个节点绘制到屏幕上。

从上面可知,浏览器只要加载到 html 结构和 css,就能够渲染出页面。

针对上面获得的结构,有3种可选方案来实现:

  • 服务端渲染
  • 利用 prerender-spa-plugin 作预渲染
  • HTML 内实现 Loading

先揭晓答案,最终我选择了第 3 种,至于为何选择,接下来挨个来看

服务端渲染

什么是服务端渲染?来自 官网 的解释是:将组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上彻底可交互的应用程序。

服务器端渲染(SSR)的优点主要在于:

  • 更好的 SEO,因为搜索引擎爬虫抓取工具能够直接查看彻底渲染的页面。
  • 更快的内容到达时间(time-to-content),特别是对于缓慢的网络状况或运行缓慢的设备。无需等待全部的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,因此你的用户将会更快速地看到完整渲染的页面。

我以前写过一篇文章,详细讨论了 如何实现一个服务端渲染项目

可是这样一个看似美好的方案,一样存在须要权衡的地方:

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数(lifecycle hook)中使用;一些外部扩展库(external library)可能须要特殊处理,才能在服务器渲染应用程序中运行。
  • 涉及构建设置和部署的更多要求。与能够部署在任何静态文件服务器上的彻底静态单页面应用程序(SPA)不一样,服务器渲染应用程序,须要处于 Node.js server 运行环境。
  • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源(CPU-intensive - CPU 密集),所以若是你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。

同时还有一个不得不考虑的问题是,因为服务端渲染模糊了先后端的界限,须要更多的服务器方面的知识,在项目落地时要全面考虑运维、后期项目交接等等,最后放弃这个方案~

利用 prerender-spa-plugin 作预渲染

预渲染 能够达到和 SSR 相似的目的,它在编译阶段,将指定的页面编译成 html,当有请求时直接将 html 内容发送给客户端,可是他也存在问题:

  • 它是在编译阶段完成的,只能编译有限的页面(例如 /, /about, /contact 等),没有办法将全部内容静态化,并且若是内容有更新,以前编译过的页面也不会获得更新。
  • 页面抖动,例如咱们首次访问的是一个动态内容 /article/id-1234 ,因为这个动态内容以前没有编译成 html,这个请求会落在 /index.html 上,请先会先把页面渲染成首页内容,当动态接口有内容后,会再次更新页面,将页面渲染成目标页,这会对用户形成困惑。

但不管怎样,这是一个不错的方案,VUE 官方也推荐这个方案,在实践的过程当中我也遇到了一些问题,同时也作了一些 记录 ,可是综合考虑仍是放弃了这个方案。

HTML 内实现 Loading

这是咱们最终选择的方案,这个方案从原理到实现都相对简单,它借助 html-webpack-plugin 将一段指定的 html 和 css 插入到模板中,在 js 和 api 请求未返回以前,以最快的速度给用户一个 loading 提示,告知用户获得了响应。

具体作法以下:

将 loading 效果的 html 拆分红 loading.htmlloading.css,分别放在 /src/preLoad/loading.html 和 /src/preLoad/loading.css

在 webpack 的 config 文件里读取这两个文件:

module.exports = {
  loading: {
    html: fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.html')),
    css: '<style>' + fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.css')) + '</style>'
  }
  // ...
}
复制代码

在 build 的配置文件里引入:

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      loading: config.loading
      // ...
    })
    // ...
  ]
  // ...
}
复制代码

在模板文件中插入变量:

<!DOCTYPE html>
<html>
<head>
  <!-- ... -->
  <%= htmlWebpackPlugin.options.loading.css %>
</head>
<body>
  <div id="app">
    <%= htmlWebpackPlugin.options.loading.html %>
  </div>
</body>
</html>
复制代码

经过这种方法,能够将一个动态的 loading 效果插入到页面中。

因为 css 会阻塞渲染,因此当咱们看到这个 loading 以前,尽可能的少加载其余的 css 和 js,采用的方案是不提取 css,因为 vue-cli 默认的设置是提取,因此须要手动修改一下:

// vue-loader.conf.js
module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: false // 不提取
  }),
  // ...
}
复制代码

这样 css 就会被编译进 js 里,经过 js 进行输出,在 loading 效果出来以前,不会阻塞页面。

最后还有一个问题须要解决,因为程序依赖各类第三方包,这些包都会打包到 vendor.js 中,使这个文件特别大,甚至超过了 1M,须要对这块进行优化,思路就是告诉 webpack 要打包不,不要将某些包打到 vendor.js里,而后咱们手动在 html 里引入这些文件。

因为如今第三方 CDN 提供了稳定的资源访问,并且借助 http/2 的多种利用特性,使的这些第三方资源加载特别快,具体作法以下:

在 webpack 的 config 定义将要从第三方引入的资源:

在 webpack 的基础配置文件里定义那些包不须要打包到 vendor.js里:

// webpack.base.conf.js
module.exports = {
  externals: {
    'vue': 'Vue',
    'vuex': 'Vuex',
    'iview': 'iview'
  }
  //...
}
复制代码

在 webpack 的 config 文件里批明第三方资源:

module.exports = {
  loading: {
    html: fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.html')),
    css: '<style>' + fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.css')) + '</style>'
  },
  css: [
    'https://cdn.jsdelivr.net/npm/iview@2.14.3/dist/styles/iview.css'
  ],
  js: [
    'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js',
    'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js',
    'https://cdn.jsdelivr.net/npm/iview@2.14.3/dist/iview.min.js'
  ]
  // ...
}
复制代码

和 loading 相似,在 build 的配置文件里引入:

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      loading: config.loading,
      externals_js: config.js, // 引入 js
      externals_css: config.css, // 引入 css
      // ...
    })
    // ...
  ]
  // ...
}
复制代码

在模板文件中插入变量:

<!DOCTYPE html>
<html>
<head>
  <!-- ... -->
  <%= htmlWebpackPlugin.options.loading.css %>
  <% for (var i in htmlWebpackPlugin.options.externals_css) { %>
  <link href="<%= htmlWebpackPlugin.options.externals_css[i] %>" rel="stylesheet">
  <% } %>
</head>
<body>
  <div id="app">
    <%= htmlWebpackPlugin.options.loading.html %>
  </div>
  

  <% for (var i in htmlWebpackPlugin.options.externals_js) { %>
  <script src="<%= htmlWebpackPlugin.options.externals_js[i] %>"></script>
  <% } %>
</body>
</html>
复制代码

经过这种方法,将比较大的包从 vendor.js 里剔除,经过第三方 CDN 引入。

优化先后的数据对比

经过这几个方面的处理,来对比一下优化先后的数据:

项目 优化前 优化后
总的资源加载量 6.7M 939K
总加载完成时间 19.05s 5.11s
首屏渲染时间 808ms 391ms
首次内容渲染 2.53s 1.62s
PageSpeed Insights 分数 13 分 83 分

因为浏览器访问及测试受限于网络及服务的不稳定性,其结果是不精确的,但作为参考值能够看到他是有很大提高。

相关文章
相关标签/搜索