Web优化训练营, 网页提速50倍

ChunPIG 3年前
   <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>