内容简介:爱康体检宝 PC(www.tijianbao.com/) 算是一个“老”项目,为什么说“老”呢,因为在前端技术日新月异,每天都有新知识、新概念,甚至新框架的今天,它还是基于vue-cli 2.x、webpack 3.x构建,显然有些老了。其次,在早期开始这个项目的时候,由于仓促上线,也没有过多的考虑性能及加载问题,目前网站上使用的图片未经过裁切,所有的库都打包到一个 vendor 里,首屏加载时间太长,等等这些问题使网站的用户体验不是太好,基于各方面的原因,决定对它进行一次优化,主要从以下几个方面:htt
爱康体检宝 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.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 引入。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
iOS Web应用开发
皮基 (Andrea Picchi) / 罗晴明 / 人民邮电出版社 / 2013-8-1 / CNY 79.00
本书介绍了如何使用Web标准技术来为iPhone和iPad制作Web应用。书中利用最前沿的Web和移动技术,演示了如何使用HTML5来完成繁重的基础工作,如何使用CSS3来制作外观,以及如何使用JavaScript来为移动网站或Web应用添加程序逻辑。 通过阅读本书,读者可以掌握面向移动的项目的开发流程。作者逐章递进,引导读者了解iOS设计与开发的各个步骤。读者可以学习到如下知识: 设......一起来看看 《iOS Web应用开发》 这本书的介绍吧!