Web优化训练营, 网页提速50倍
ChunPIG
8年前
<h2><strong>前言</strong></h2> <p>我们将通过一个完整的实例, 一步步的优化加载, 渲染等各方面的体验.</p> <h2><strong>开始</strong></h2> <p>首先我们先看一下项目的文件构成</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/cf7e7f1e7e9b22e13b26c81a28d163af.png"></p> <p>这之中包含了一个基本网页的元素, js(React App), css, 还有图片.</p> <p>我们先来看一下来serve整个网页的部分.</p> <p>server.js</p> <pre> <code class="language-javascript">'use strict'; const fs = require('fs'); const path = require('path'); const koa = require('koa'); const app = koa(); app.use(function* (next) { const file = this.path.slice(1) || 'index.html'; try { const content = yield cb => fs.readFile(path.resolve('./dist', file), cb); this.body = content; this.type = path.extname(file).slice(1); this.status = 200; } catch (e) { this.status = 404; } yield next; }); app.listen(process.env.PORT || 3000);</code></pre> <p>这段代码只是简单的将 dist 目录下的文件给转发一下.</p> <p>打开网页便可以看到相关加载情况.</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b43e1ffc9483b15b6c2bd56af10be483.png"></p> <p>我们可以看到, 整个 app.js 共277kb, 在模拟3G网络的情况下(蓝色框框),每次加载需要花费999ms, 其中下载花费了911ms(红色框框).</p> <p>接下来我们将逐步优化, 然后每次将结果进行比较.</p> <h2><strong>优化(一) --- 304</strong></h2> <p>网页加载优化中最常见的就是 304 Not Modified 了, 具体机制是浏览器发起请求, headers中包含 If-Modified-Since ,(如无缓存, 则无此头字段), 服务器对比硬盘上(或内存中)文件最后修改的时间, 如果小于或等于请求的时间, 则返回304. 否则, 则返回200, 并加上 Last-Modified 字段, 告诉客户端下次请求可以尝试请求是否有缓存.</p> <p>具体代码如下:</p> <pre> <code class="language-javascript">app.use(function* () { const file = path.resolve(__dirname, path.resolve('dist', this.path.slice(1) || 'index.html')); const headers = this.headers; let ifLastModified = this.headers['if-modified-since']; if (ifLastModified) { ifLastModified = new Date(ifLastModified); } try { const stat = yield cb => fs.stat(file, cb); const now = Date.now(); if (ifLastModified && file !== path.resolve(__dirname, path.resolve('dist/index.html'))) { if (ifLastModified >= stat.mtime) { this.status = 304; return; } } console.log(file) const content = yield cb => fs.readFile(file, cb); this.body = content; this.type = path.extname(file).slice(1); this.status = 200; this.set('Last-Modified', stat.mtime); } catch (e) { this.status = 404; } });</code></pre> <p>(模拟实际情况中, 首页会动态生成, 加入一些广告,追踪或个性化数据, index.html 并未缓存)</p> <p>最终效果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/9f8203e97ae68674f036ee010512d5cf.png"></p> <p>我们可以看见, 下载时间为2ms, 可以几乎忽略掉(只有HTTP Headers), 总共的加载时间也只有了120ms, 相比之前, 整整少了 869ms.</p> <p>但是, 我们满足了吗?</p> <h2><strong>优化(二) --- 分别打包</strong></h2> <p>我们可以注意到, 我们打包出来最终只有一个js文件, 当依赖变多后(此例中只有react和react-dom, 每次修改都导致整个js文件被重新请求.所以我们想要把不同的library(甚至是项目内部公用的代码模块)提取出来.</p> <p>我们首先要创建一个 webpack.vendors.config.js 来构建这些library, 或者vendor.</p> <pre> <code class="language-javascript">const path = require('path'); const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require('webpack'); const ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"', }, }), new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, screw_ie8: true, drop_console: true, drop_debugger: true, }, }), new webpack.DllPlugin({ path: path.resolve(__dirname, 'dist/vendor/[name]-manifest.json'), name: '[name]', context: '.', }), ], devtool: 'hidden-source-map', entry: { 'react': ['react', 'react-dom'], }, output: { path: path.resolve(__dirname, 'dist/vendor'), filename: '[name].js', library: '[name]', }, };</code></pre> <p>注意到</p> <pre> <code class="language-javascript">entry: { 'react': ['react', 'react-dom'], },</code></pre> <p>意味着我们可以将同一类型的包打包成一个js文件.</p> <p>当然, 我们也要对 webpack.production.js 做一些修改.</p> <pre> <code class="language-javascript">const dlls = fs.readdirSync(path.resolve(__dirname, 'dist/vendor/')) .filter(file => path.extname(file) === '.js') .map(file => path.basename(file, '.js')) const dllReferencePlugins = dlls .map(dll => new webpack.DllReferencePlugin({ context: '.', manifest: require(`./dist/vendor/${dll}-manifest.json`), }) ); module.exports = { plugins: dllReferencePlugins.concat([ ... ]), ... }</code></pre> <p>在这, 我们将自动扫描 vendor 目录下面的文件, 自动将所有的vendor加载进来.</p> <p>这样我们就实现了分包加载(还有一些细节的修改, 包括 index.html , 请参见github上, step-2 分支)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e536a150f1405f2e63377ab8c42a0799.png"></p> <p>效果还是不错的, app.js 单独加载只需要400多ms, 比起所有依赖一起加载要快了至少一半以上.</p> <p>对于一般类型网站, 优化到这已经可以取得非常不错的效果了, 但是对于大型网站来说, 我们可以做的还有很多.</p> <h2><strong>优化(三) --- 强制缓存</strong></h2> <p>我们可以注意到优化一种, 一个304的请求仍然花掉了100多毫秒, 对于大型网站, 资源特别多的情况, 这仍然是一个不小的开支. 那我们可以把这个省掉吗? 答案是可以的.</p> <p>浏览器缓存当中, 还有一个特别的字段. Expires , 它可以指定文件的过期时间, 直到那一刻位置, 浏览器都不会再重新发起请求, 而是直接从本地缓存中读取.</p> <p>但是, 这仍旧需要每隔一段时间去请求. 我们该如何做呢? 答案就是, 设置超长的缓存时间, 例如10年. 但是这样我们便无法更新任何内容了. 我们该如何用到这样的特性, 而又很方便的更新呢.</p> <p>我们可以给文件名加上 hash特征值 , 这样只有当文件内容有改动时, 才会重新加载, 而且这样适合于分布式CDN的, 非覆盖式的发布, 可以使其在引用页面(首页)已经改变的情况下(当前服务器已经发布), 才会用到新资源, 而访问到未发布的服务器时, 还是会引用老的资源, 使得发布再也不需要熬夜</p> <p>具体细节改动见git branch step-3.</p> <p style="text-align:center">实现效果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d43dbb2eefc7df4de9881bed55ab3b7e.png"></p> <p>可以从蓝色方框出看见, 缓存已经生效, 而整体的读取时间才只有20毫秒不到.</p> <p>从原始的1000毫秒, 到现在的20毫秒, 简简单单的三个步骤便可以让你的网页加载提速50倍</p> <h2><strong>扩展阅读</strong></h2> <p>1.在实际生产中, 我们通常看到的是加载的CDN域名, 这是为何呢?</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/bb6a95552b570dd1fb2e6a8ddec5ea0b.png"></p> <p>这是因为, 一个大型的网站, 请求当中会带上很多Cookie, 有的甚至于接近1KB, 而100个图片的加载, 就是整整100KB. 通过第三方域名(不同于当前域名), 我们可以节省掉许多不必要的请求头, Cookie头. 同样达到提速的目的</p> <p>2.还有一种情况是, 资源分布在不同的服务器上</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6cfdbe84059684acc1f0241a42c0efa3.png"></p> <p>这是因为浏览器对于同一域名下资源的并行下载数量有限制.</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b5e99116f9f7aac412875b0d807482b1.png"></p> <p>使用不同的资源服务器可以避开这种限制, 加大下载并发数. 但是, 这样同样带来的缓存命中率的问题, 所以还需要存储用户缓存相关的数据. 合理的利用下, 对于页面整体的加载速度还是很有好处的.</p> <p>3.其他的方法</p> <p>在技术飞速发展的当下, 还有很多技术都是可以对终端用户的体验带来提升的.</p> <ul> <li>BigPipe + Server-Side Rendering, 加速首页加载速度</li> <li>Goole AMP</li> <li>HTTP/2</li> </ul> <p> </p> <p>来自:http://tech.dianwoda.com/2016/11/01/web-load-optimization-step-by-step/</p> <p> </p>