爱康体检宝 PC(www.tijianbao.com/) 算是一个“老”项目,为何说“老”呢,由于在前端技术突飞猛进,天天都有新知识、新概念,甚至新框架的今天,它仍是基于vue-cli 2.x、webpack 3.x构建,显然有些老了。其次,在早期开始这个项目的时候,因为仓促上线,也没有过多的考虑性能及加载问题,目前网站上使用的图片未通过裁切,全部的库都打包到一个 vendor 里,首屏加载时间太长,等等这些问题使网站的用户体验不是太好,基于各方面的缘由,决定对它进行一次优化,主要从如下几个方面进行:javascript
下面分别展开来讲php
https 主要带来安全性方面的提高,并且 http/2 依赖于 https,只有使用 https 协议的站点能够升级 http/2 协议。css
http/2 带来了一系列的改动和优化,主要以下:html
这里有一篇来自 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
具体 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 只是单纯的作为文件存储服务器,因此没有办法动态裁切。
不过天无绝人之路,能够经过 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…
使用图片懒加载 主要依赖 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 版本,有效下降文件大小
经过这一系列操做,能够更进一种下降没必要要的资源加载量。
按说 webpack 3 用的好好的,为何要升级到 4 呢,缘由仍是由于新版本给咱们带来了诸多好片,并且目前已是稳定版本,主要有如下内容:
升级过程当中可能会遇到种种问题,好在有个 官方升级指南 能够帮咱们覆盖掉一部分,但这会指南过于简明扼要,具体到项目中还会有不少坑,好在经过错误提示结合伟大的google,最终都能找到答案(若是你用 baidu ,颇有可能最终爬不上来 :< ...),也能够结合网上其余一些升级方面的文章,都会颇有帮助,我这里就不细述了。
总在来讲升级 wp 4 会花一些时间,但带来的效率和性能提高绝对值得。
对于 web 单页面应用而言,一个很是大的痛点就是在首次加载时加载的资源量过大,致使用户在第一次访问时出如今白屏时间较长,如何优化这个体验是总体优化中的重中之重,因此放在最后来讲。
前面已经说了,经过减小图片大小、升级 webpack 四、优化公共资源包等等手段,都是为了这个服务(固然了,也不全是:>),这些手段都是对资源进行操做。当对这全部的资源进行了操做,如何合理处理这些资源,就到了浏览器的渲染机制,如何经过优化渲染过程,提升首屏渲染速度,是咱们下一步要考虑的。
浏览器渲染页面的过程,主要分为五步(略过请求部分,只讨论请求到资源后浏览器如何处理):
从上面可知,浏览器只要加载到 html 结构和 css,就能够渲染出页面。
针对上面获得的结构,有3种可选方案来实现:
先揭晓答案,最终我选择了第 3 种,至于为何选择,接下来挨个来看
什么是服务端渲染?来自 官网 的解释是:将组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上彻底可交互的应用程序。
服务器端渲染(SSR)的优点主要在于:
我以前写过一篇文章,详细讨论了 如何实现一个服务端渲染项目。
可是这样一个看似美好的方案,一样存在须要权衡的地方:
同时还有一个不得不考虑的问题是,因为服务端渲染模糊了先后端的界限,须要更多的服务器方面的知识,在项目落地时要全面考虑运维、后期项目交接等等,最后放弃这个方案~
预渲染 能够达到和 SSR 相似的目的,它在编译阶段,将指定的页面编译成 html,当有请求时直接将 html 内容发送给客户端,可是他也存在问题:
但不管怎样,这是一个不错的方案,VUE 官方也推荐这个方案,在实践的过程当中我也遇到了一些问题,同时也作了一些 记录 ,可是综合考虑仍是放弃了这个方案。
这是咱们最终选择的方案,这个方案从原理到实现都相对简单,它借助 html-webpack-plugin 将一段指定的 html 和 css 插入到模板中,在 js 和 api 请求未返回以前,以最快的速度给用户一个 loading 提示,告知用户获得了响应。
具体作法以下:
将 loading 效果的 html 拆分红 loading.html
和 loading.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 分 |
因为浏览器访问及测试受限于网络及服务的不稳定性,其结果是不精确的,但作为参考值能够看到他是有很大提高。