首页 > WEB开发 > Web优化训练营, 网页提速50倍
2016
11-03

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

前言

我们将通过一个完整的实例, 一步步的优化加载, 渲染等各方面的体验.

开始

首先我们先看一下项目的文件构成

这之中包含了一个基本网页的元素, js(React App), css, 还有图片.

我们先来看一下来serve整个网页的部分.

server.js

'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);

这段代码只是简单的将 dist 目录下的文件给转发一下.

打开网页便可以看到相关加载情况.

我们可以看到, 整个 app.js 共277kb, 在模拟3G网络的情况下(蓝色框框),每次加载需要花费999ms, 其中下载花费了911ms(红色框框).

接下来我们将逐步优化, 然后每次将结果进行比较.

优化(一) --- 304

网页加载优化中最常见的就是 304 Not Modified 了, 具体机制是浏览器发起请求, headers中包含 If-Modified-Since ,(如无缓存, 则无此头字段), 服务器对比硬盘上(或内存中)文件最后修改的时间, 如果小于或等于请求的时间, 则返回304. 否则, 则返回200, 并加上 Last-Modified 字段, 告诉客户端下次请求可以尝试请求是否有缓存.

具体代码如下:

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;
    }
});

(模拟实际情况中, 首页会动态生成, 加入一些广告,追踪或个性化数据, index.html 并未缓存)

最终效果:

我们可以看见, 下载时间为2ms, 可以几乎忽略掉(只有HTTP Headers), 总共的加载时间也只有了120ms, 相比之前, 整整少了 869ms.

但是, 我们满足了吗?

优化(二) --- 分别打包

我们可以注意到, 我们打包出来最终只有一个js文件, 当依赖变多后(此例中只有react和react-dom, 每次修改都导致整个js文件被重新请求.所以我们想要把不同的library(甚至是项目内部公用的代码模块)提取出来.

我们首先要创建一个 webpack.vendors.config.js 来构建这些library, 或者vendor.

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]',
  },
};

注意到

entry: { 'react': ['react', 'react-dom'],
  },

意味着我们可以将同一类型的包打包成一个js文件.

当然, 我们也要对 webpack.production.js 做一些修改.

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([
   ...
  ]),
  ...
}

在这, 我们将自动扫描 vendor 目录下面的文件, 自动将所有的vendor加载进来.

这样我们就实现了分包加载(还有一些细节的修改, 包括 index.html , 请参见github上, step-2 分支)

效果还是不错的, app.js 单独加载只需要400多ms, 比起所有依赖一起加载要快了至少一半以上.

对于一般类型网站, 优化到这已经可以取得非常不错的效果了, 但是对于大型网站来说, 我们可以做的还有很多.

优化(三) --- 强制缓存

我们可以注意到优化一种, 一个304的请求仍然花掉了100多毫秒, 对于大型网站, 资源特别多的情况, 这仍然是一个不小的开支. 那我们可以把这个省掉吗? 答案是可以的.

浏览器缓存当中, 还有一个特别的字段. Expires , 它可以指定文件的过期时间, 直到那一刻位置, 浏览器都不会再重新发起请求, 而是直接从本地缓存中读取.

但是, 这仍旧需要每隔一段时间去请求. 我们该如何做呢? 答案就是, 设置超长的缓存时间, 例如10年. 但是这样我们便无法更新任何内容了. 我们该如何用到这样的特性, 而又很方便的更新呢.

我们可以给文件名加上 hash特征值 , 这样只有当文件内容有改动时, 才会重新加载, 而且这样适合于分布式CDN的, 非覆盖式的发布, 可以使其在引用页面(首页)已经改变的情况下(当前服务器已经发布), 才会用到新资源, 而访问到未发布的服务器时, 还是会引用老的资源, 使得发布再也不需要熬夜

具体细节改动见git branch step-3.

实现效果:

可以从蓝色方框出看见, 缓存已经生效, 而整体的读取时间才只有20毫秒不到.

从原始的1000毫秒, 到现在的20毫秒, 简简单单的三个步骤便可以让你的网页加载提速50倍

扩展阅读

1.在实际生产中, 我们通常看到的是加载的CDN域名, 这是为何呢?

这是因为, 一个大型的网站, 请求当中会带上很多Cookie, 有的甚至于接近1KB, 而100个图片的加载, 就是整整100KB. 通过第三方域名(不同于当前域名), 我们可以节省掉许多不必要的请求头, Cookie头. 同样达到提速的目的

2.还有一种情况是, 资源分布在不同的服务器上

这是因为浏览器对于同一域名下资源的并行下载数量有限制.

使用不同的资源服务器可以避开这种限制, 加大下载并发数. 但是, 这样同样带来的缓存命中率的问题, 所以还需要存储用户缓存相关的数据. 合理的利用下, 对于页面整体的加载速度还是很有好处的.

3.其他的方法

在技术飞速发展的当下, 还有很多技术都是可以对终端用户的体验带来提升的.

  • BigPipe + Server-Side Rendering, 加速首页加载速度
  • Goole AMP
  • HTTP/2

 

来自:http://tech.dianwoda.com/2016/11/01/web-load-optimization-step-by-step/

编程技巧