12步建立高性能Web APP

如今,Web App 日益重视用户的交互体验,了解性能优化的方式则能够有效提升用户体验。阅读和实践下面的性能优化技巧,能够帮你改善应用的流畅度、渲染时间和其余方面的性能表现。javascript

概述

对 Web App 进行性能优化是一份冗杂沉重的工做,这不只是由于构建一个 Web App 须要先后端协做,并且须要多方面的技术栈:数据库、后端、前端,须要运行在多种平台:iOS,安卓,Chrome,Firefox,Edge。这太复杂了!不过,仍是有一些历经实践的通用方式能够用来优化 Web App 的性能。在接下来的小节中,咱们将逐步介绍相关的细节。css

一份来自 Bing 的研究代表,页面加载时间每增长 10ms,每一年就会减小 $250k 的收入。 ———— Rob Trace 和 David Walp,来自微软的高级产品经理html

过早优化

性能优化的难点在于找出开发中值得优化的地方。Donald Knuth 说过一句经典的话:“过早的优化是一切罪恶的根源”。这句话背后的意思是说:花费大量时间改善 1% 的性能毫无心义。同时,某些优化方案反倒影响了可读性或可维护性,甚至引入了新的问题。换言之,性能优化不该该被视为“榨干应用程序性能的方法”,而应该视为“对性能和收益的平衡性所进行的探索”。在践行如下优化技巧时必定要牢记,盲目优化会影响生产效率,甚至得不偿失。最好的方式是使用分析工具来查找性能瓶颈,并在性能优化和开发效率、可维护性等方面保持平衡。前端

开发者浪费了大量的时间去思考或者担忧程序的执行速度,但实际上从调试和后期维护的角度看,这些优化措施每每会带来严重的负面影响。咱们应该着重 97% 的运行表现:过早的性能优化是一切罪恶的根源。固然,咱们也不该该放弃 3% 的痛点。 ———— Donald Knuthjava

文件压缩和模块打包

JavaScript 一般是直接使用源码的方式分发的,而源码解析起来每每要慢于字节码。对于小脚原本说,二者解析的速度并不大,但对于大的应用程序来讲,则会明显影响应用程序的启动速度。解决这一痛点,正是 WebAssembly 的出发点之一,它将大幅改善程序的启动速度。文件压缩是剔除文件中无用字符的流程,虽然处理后的代码丧失了可读性,但提升了浏览器的解析速度。react

另外一方面,模块打包能够将不一样的脚本合并为一个脚本,从而下降 HTTP 请求,减小资源加载时间。一般来讲,这种工做都会交给相应的工具来处理,好比 Webpackwebpack

function insert(i) { document.write("Sample " + i); } for(var i = 0; i < 30; ++i) { insert(i); } 

压缩以后:nginx

!function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){function e(r){document.write("Sample "+r)}for(var o=0;30>o;++o)e(o)}]); //# sourceMappingURL=bundle.min.js.map 

深度打包

使用 Webpack,咱们也能够压缩 CSS 和合并图片,进一步改善程序的启动速度。更多有关 Webpack 的信息请参考官方文档git

按需加载

按需加载资源或者说懒加载资源(特别是图片)对优化 Web App 的性能有很大帮助。对于图片较多的页面,使用懒加载一般有如下三点好处:angularjs

  • 减小并发请求,缓解服务器压力,提升加载速度
  • 减小浏览器的内存占用率
  • 下降服务器的负载

图片或其余资源懒加载的方案通常是,在程序启动时加载首屏资源,在页面滚动时持续加载即将进入视口的资源。因为这种方法每每须要与页面结构和开发方式相协调,因此经常使用现有的插件和扩展来实现惰性加载。举例来讲,react-lazy-load 是一个基于 React 的图片惰性加载插件:

const MyComponent = () => ( <div> Scroll to load images. <div className="filler" /> <LazyLoad height={762} offsetVertical={300}> <img src='http://apod.nasa.gov/apod/image/1502/HDR_MVMQ20Feb2015ouellet1024.jpg' /> </LazyLoad> (...) 

一个典型的按需加载实例就是谷歌的图片搜索工具,点击这一连接并滚动页面,打开开发者工具注意资源的加载时间。

array-ids

若是你正在使用 React 、 Ember 、 Angular 或者其余操做 DOM 的第三方库,那么使用 array-ids(或者是 Angular 1.x 中的track-by 特性)能够有效提升页面性能,对动态网站的性能优化尤其突出。从最新的基准测试中咱们也能够看出其中的优点:More Benchmarks: Virtual DOM vs Angular 1 & 2 vs Mithril.js vs cito.js vs The Rest (Updated and Improved!)

array-ids

其背后的核心概念就是尽量多地重复利用现有节点。Array-ids 便于 DOM 操做引擎根据获取到的 DOM 节点与真实的节点相匹配。若是没有 array-id 或者 track-by,大多数第三方库都会简单粗暴的删除节点而后再建立节点,这会严重影响程序的执行速度。

缓存

缓存经常使用来存储频繁调用的数据,当缓存后的数据再次被调用时,就能够由缓存直接提供数据,提升数据的响应速度。一般来讲,一个 Web App 都是由多个组件构成的,在这些组件中都能发现缓存的影子。好比动态内容服务器和客户端之间使用的缓存,经过减小通用请求下降服务器负载,能够改善页面的响应时间;好比代码中的缓存处理,能够优化某些通用的脚本访问模式。此外,还有数据库缓存和长进程缓存等。

简而言之,缓存是改善应用程序响应速度和下降 CPU 负载的有效方式。在一个开发体系中,最难的不是如何使用缓存,而是找出哪里适合使用缓存。对于这一问题,我仍是建议使用事件分析工具(profiler):找出性能瓶颈,检测缓存是否成功,测试缓存是否容易失效……这些问题都须要历经实践才能得出有效的结论。

使用缓存能够优化资源加载,好比,使用 basket.js 利用本地存储缓存应用的脚本,在第二次调用资源时能够迅速从本地存储中得到相应的资源。

Amazon CloudFront 是如今比较流行的一项缓存服务。CloudFront 的工做机制相似内容分发网络(CDN),能够为动态内容设置缓存。

HTTP/2

目前,已经有愈来愈多的浏览器支持 HTTP/2。HTTP/2 的优点在于它与服务器的并发链接,好比,若是须要加载的小型资源(前提是你不对资源进行打包)比较多,HTTP/2 在响应时间和性能上都要远远优胜于 HTTP/1。你能够点击 Akamai 的 HTTP/2 示例 查看二者的区别。

HTTP/2

性能剖析

性能剖析是应用程序进行性能优化的重要步骤。如上文所说,盲目地优化应用程序每每会下降生产力、产生新的痛点且难以维护。性能剖析的做用就是要找出应用程序中潜在的风险区域。

对 Web 应用程序来讲,响应速度是一个很是重要的衡量指标,因此开发者都会尽量地去提升资源的加载速度和页面的渲染速度。Chrome 浏览器提供了一系列优秀的性能剖析工具,其中最经常使用的就是开发者工具中的 timeline 和 network,善用它们能够准肯定位有关响应速度的风险区域。

性能剖析

timeline 面板便于快速查找耗时操做。

性能剖析

network 面板便于定位由请求时间和串行加载引发的响应速度问题。

此外,若是合理分析内存的使用率,也将有效提升应用程序的性能。若是你的页面中有大量的视觉元素(好比动态的表格)或者大量的交互元素(好比游戏),那么对内存使用的剖析就能够有效减小卡顿,提升帧速。若是你想了解如何在 Chrome 开发者工具中进行内存剖析,请参考这篇文章:《4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them 》

Chrome 开发者工具也能够对 CPU 的使用进行剖析,更多详细信息请参考来自谷歌文档的这篇文章:《 Profiling JavaScript Performance》

CPU剖析

找出性能的核心痛点,才能让你更加高效地进行性能优化。

相对而言,对后端进行性能剖析稍显困难。通常而言,从最耗时的请求入手查找相应的服务器是个不错的方法。这里并无推荐任何有关后端的性能剖析工具,这是由于具体的剖析工具要视具体的后端技术栈而定。

算法

在大多数状况下,选择更高效的算法能够比局部优化得到更佳的收益。从某种意义上说,对 CPU 和内存进行性能剖析有助于帮助开发者找出应用程序中较大的性能瓶颈。若是这些瓶颈并非由代码的错误引发的,那颇有可能就是算法的问题。

负载均衡

在上文的缓存一节中,简单提到了内容分发网络(CDN)的概念。根据服务器或者地理区域分发负载能够有效提升资源的响应速度,这一优点在处理并发连接时尤其明显。

简而言之,负载均衡相似于一种轮询方案,基于反向代理服务器 nginx 或者成熟的分发网络(好比 Cloudflare 和 Amazon CloudFront 构建。

负载均衡

为了实现负载均衡,须要将动态内容和静态内容进行分离,便于执行并行链接。换言之,串行访问削弱了负载均衡检索最佳路径并进行分发的能力。此外,并行加载资源还能够加快应用程序的启动速度。

负载均衡也能够构建的很精细。若是数据模型不可以很好地与最终的一致性算法或缓存保持良好的匹配关系,那么必将致使诸多问题。幸运的是,大多数的应用程序所请求的数据都是一个缩减集,该缩减集自己具备较高级别的一致性。若是你的应用程序尚未具有这样的能力,那么你须要考虑重构它了。

同构 JavaScript

对于 Web 应用程序来讲,一个加强用户体验的法门就是减小启动时间或者减小首屏渲染时间,这一点对于须要在客户端执行大量逻辑操做的单页应用尤其重要。在客户端执行的逻辑操做越多,一般意味着须要在首屏渲染前加载更多的资源。同构 JavaScript 就是用来解决这一问题的:JavaScript 能够同时在客户端和服务端执行,因此能够在服务端渲染出来首屏,而后将其发送给客户端,再由客户端的 JavaScript 接手剩下的逻辑处理。这一方案限制了服务端只能基于 JavaScript 框架,但能够提升用户体验。目前,在Meteor.js 中已经能够直接使用这一方式了。此外,在 React 框架中也能够采用这种方式,代码以下所示:

var React = require('react/addons'); var ReactApp = React.createFactory(require('../components/ReactApp').ReactApp); module.exports = function(app) { app.get('/', function(req, res){ // React.renderToString takes your component // and generates the markup var reactHtml = React.renderToString(ReactApp({})); // Output html rendered by react // console.log(myAppHtml); res.render('index.ejs', {reactOutput: reactHtml}); }); }; 

下面是 Meteor.js 的简单示例:

if (Meteor.isClient) { Template.hello.greeting = function () { return "Welcome to myapp."; }; Template.hello.events({ 'click input': function () { // template data, if any, is available in 'this' if (typeof console !== 'undefined') console.log("You pressed the button"); } }); } if (Meteor.isServer) { Meteor.startup(function () { // code to run on server at startup }); } 

若是你开发的是大中型复杂应用且支持同构发布,那么能够尝试一下这种方式,效果极可能使人震撼。

索引

若是数据库查询占据了太多的执行时间,那么你应该考虑优化数据库的执行速度了。每种数据库和数据模型都各有特点。数据库优化有多种方向:数据模型、数据库类型以及其余配置,因此优化起来并不简单。不过,咱们仍是有一些通用的优化技巧,好比说:索引。索引根据数据库的数据建立快速访问的数据结构,改善对特定数据的检索速度。如今大多数的数据库都支持索引功能,

在使用索引优化数据库以前,你应该研究当前应用程序的访问模式,分析最经常使用到的查询是什么,哪个键或者字段会被频繁查询等等。

编译工具

JavaScript 技术栈日益复杂,这也推进了语言自己的进步。不幸的是,JavaScript 的发展目前还要受限于用户的访问环境。虽然 ECMAScript 2015 已经对 JavaScript 作出了诸多改进,可是开发者尚不能直接遵循这一规范的代码。针对这一问题,也就衍生出了诸多编译工具,这些工具经常使用于将 ECMAScript 2015 的代码转换为 ECMAScript 5 的代码。此外,模块打包和文件压缩也加入到了编译过程,最终用于生成线上版本的代码。这些工具将代码转换为了一个受限的版本,间接影响到了最终代码的执行效率。谷歌开发者 Paul Irish 测试了代码转换对性能和文件大小的影响,详情请点击连接。虽然大多数状况下影响甚微,但这些差别仍然值得引发注意,由于随着应用程序的复杂大增高,这些差别也将日益增大。

阻塞渲染

JavaScript 和 CSS 资源的加载都会阻塞页面的渲染过程。经过某些技巧,开发者能够尽快加载 JavaScript 和 CSS 资源,从而让浏览器尽快显示网站的内容。

对 CSS 来讲,本质上符合当前页面媒体属性的 CSS 规则会具备较高的处理优先级。页面的媒体属性由 CSS 的媒体查询进行匹配。媒体查询通知浏览器哪个 CSS 脚本针对哪种媒体属性。举例来讲,相对于当前屏幕显示的 CSS,用于打印的 CSS 的优先级较低。

能够为 <link> 标签设置与媒体查询有关的属性:

<link rel="stylesheet" type="text/css" media="only screen and (max-device-width: 480px)" href="mobile-device.css" /> 

对 JavaScript 来讲,关键是恰当地使用内嵌 JavaScript(即在 HTML 中的 JavaScript)。内嵌 JavaScript 应该尽量简短,且不能阻塞对页面其余部分的阻塞。换言之,位于 HTML 文档树之中的内嵌 JavaScript 会阻塞 HTML 脚本的解析,强制解析引擎直到脚本执行完成才能继续解析。若是 HTML 树中有大量这种阻塞脚本或者阻塞时间过长,势必严重破坏应用程序的用户体验。内嵌 JavaScript 有助于防止网络获取过多的脚本。对于反复用到的脚本,或者体积较大的脚本,不建议使用内联形式。

一种有效防止 JavaScript 阻塞 HTML 解析的方法是以异步的方式加载 <script> 标签。这种方式限制了咱们队 DOM 的访问(没法使用 document.write),但可让浏览器在解析和渲染页面的时候无需考虑 JavaScript 的执行状态。换言之,为了获取最佳的启动速度,应该确保全部非必需的脚本都要以异步的形式加载:

<script src="async.js" async></script> 

servce workers 和 stream

Jake Archibald 的最新文章 对提升渲染速度提出了一个颇有意思的方案:结合 service workers 和 stream 进行页面渲染。结果至关使人信服:

不幸的是,这一技巧所用到的 API 尚在变化之中,因此还不能应用于实际开发中。这一技巧的核心是在网站和客户端之间存放一个 service worker。service worker 能够用于缓存数据(好比网站的头部等不常变更的部分),避免网络查找失败。若是缓存数据丢失,能够经过 stream 快速获取。

扩展阅读

更多有关性能优化的信息和工具请参考如下连接:

结论

随着应用程序变得愈来愈庞大和复杂,性能优化在 Web 开发中的地位也愈来愈重要。针对性的性能优化相当重要,有助于下降时间成本和维护成本。Web 应用程序历经发展,其做用已经再也不是单一的内容展示,学习通用的性能优化模式,能够将一个难以使用的应用程序转为一个易于上手的工具。没有任何规则是绝对的,只有不断研究和剖析技术栈的深层次逻辑,才能合理进行性能优化。

相关文章
相关标签/搜索