记一次 VUE 项目优化实践

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

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

https 主要带来安全性方面的提升,而且 http/2 依赖于 https,只有使用 https 协议的站点可以升级 http/2 协议。

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

  • 每个服务器只用一个连接。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模块

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

配置服务器 conf 文件

server {
 listen 443 ssl http2 default_server;

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

然后重启服务器,完成升级

合理控制缓存

缓存对于 web 应用程序至关重要,合理控制缓存可以有效提升 web 性能,我们之前有些域下未做明确的缓存管理,虽然浏览器有默认的缓存机制,但是由于默认的机制未必能满足我们的要求,而且各浏览器的默认机制不同,可能造成 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 off;

# 不缓存接口
        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/commodity/9… ,这是一个原始资源,可能是一个很大的图片,直接引用会造成浪费

通过自动裁切,我们可以引用指定宽度的图片:domain.url/commodity/9… ,通过在 url 后加 !/fw/200,可以引用宽度为 200px 的图片,最大限度上节省资源

对于支持 webp 的浏览器,还可以让其输出 webp 版本:domain.url/commodity/9… ,通过关键字 /format/webp 指定输出的资源格式为 webp

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

针对来自 idc 的图片资源

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

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

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

二、配置 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/img.… 得到宽为 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 4、优化公共资源包等等手段,都是为了这个服务(当然了,也不全是:>),这些手段都是对资源进行操作。当对这所有的资源进行了操作,如何合理处理这些资源,就到了浏览器的渲染机制,如何通过优化渲染过程,提高首屏渲染速度,是我们下一步要考虑的。

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

  • 处理 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 上,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/[email protected]/dist/styles/iview.css'
  ],
  js: [
    'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js',
    'https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js',
    'https://cdn.jsdelivr.net/npm/[email protected]/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 分

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

猜你喜欢

转载自juejin.im/post/5bc830a8f265da0abe272ccf