网站性能优化是必须的技能,而且需要长期积累,以下是我自己总结的一些性能优化的策略,主要分为几个方面:

  1. 网络请求优化
  2. 页面渲染优化
  3. JS阻塞性能与内存泄漏
  4. 负载均衡

1 网络请求优化

1.1 浏览器缓存

浏览器在向服务器发起请求前,会先查询本地是否有相同的文件,如果有,就会直接拉取本地缓存,这和我们在后台部署的Redis、Memcache类似,都是起到了中间缓冲的作用,我们先看看浏览器处理缓存的策略:

浏览器默认的缓存是放在内存内的,内存里的缓存会因为进程的结束或者说浏览器的关闭而被清除,而存在硬盘里的缓存才能够被长期保留下去。很多时候,我们在network面板中各请求的size项里,会看到两种不同的状态:from memory cache 和 from disk cache,前者指缓存来自内存,后者指缓存来自硬盘。而控制缓存存放位置的,不是别人,就是我们在服务器上设置的Etag字段。在浏览器接收到服务器响应后,会检测响应头部(Header),如果有Etag字段,那么浏览器就会将本次缓存写入硬盘中。

以Nginx为例,设置Etag

1
2
etag on;   //开启etag验证
expires 14d; //设置缓存过期时间为14天

打开我们的网站,在chrome devtools的network面板中观察我们的请求资源,如果在响应头部看见Etag和Expires字段,就说明我们的缓存配置成功了。

在我们配置缓存时一定要切记,浏览器在处理用户请求时,如果命中强缓存,浏览器会直接拉取本地缓存,不会与服务器发生任何通信,也就是说,如果我们在服务器端更新了文件,并不会被浏览器得知,就无法替换失效的缓存。所以我们在构建阶段,需要为我们的静态资源添加md5 hash后缀,避免资源更新而引起的前后端文件无法同步的问题。

1.2 资源打包压缩

我们之前所作的浏览器缓存工作,只有在用户第二次访问我们的页面才能起到效果,如果要在用户首次打开页面就实现优良的性能,必须对资源进行优化。我们常将网络性能优化措施归结为三大方面:减少请求数、减小请求资源体积、提升网络传输速率。现在,让我们逐个击破:

以Webpack为例

  • 压缩JS

    1
    new webpack.optimize.UglifyJsPlugin()
  • 压缩Html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    new HtmlWebpackPlugin({
    template: __dirname + '/views/index.html', // new 一个这个插件的实例,并传入相关的参数
    filename: '../index.html',
    minify: {
    removeComments: true,
    collapseWhitespace: true,
    removeRedundantAttributes: true,
    useShortDoctype: true,
    removeEmptyAttributes: true,
    removeStyleLinkTypeAttributes: true,
    keepClosingSlash: true,
    minifyJS: true,
    minifyCSS: true,
    minifyURLs: true,
    },
    chunksSortMode: 'dependency'
    })

我们在使用html-webpack-plugin 自动化注入JS、CSS打包HTML文件时,很少会为其添加配置项,这里我给出样例,大家直接复制就行。

PS:这里有一个技巧,在我们书写HTML元素的src 或 href 属性时,可以省略协议部分,这样也能简单起到节省资源的目的。

  • 压缩CSS

在使用webpack的过程中,我们通常会以模块的形式引入css文件(webpack的思想不就是万物皆模块嘛),但是在上线的时候,我们还需要将这些css提取出来,并且压缩,这些看似复杂的过程只需要简单的几行配置就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module: {
rules: [..., {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: {
loader: 'css-loader',
options: {
minimize: true
}
}
})
}]
}

  • 使用webpack3的新特性:ModuleConcatenationPlugin

    1
    new webpack.optimize.ModuleConcatenationPlugin()
  • 把prod环境的shouldUseSourceMap设置为false,去掉build生成的map文件

    1
    devtool: shouldUseSourceMap ? 'source-map' : false,

最后,我们还应该在服务器上开启Gzip传输压缩,它能将我们的文本类文件体积压缩至原先的四分之一,效果立竿见影,还是切换到我们的nginx配置文档,添加如下两项配置项目:

1
2
gzip on;
gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xml text/javascriptapplication/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;

如果你在网站请求的响应头里看到这样的字段,那么就说明咱们的Gzip压缩配置成功啦:

【!!!特别注意!!!】不要对图片文件进行Gzip压缩!不要对图片文件进行Gzip压缩!不要对图片文件进行Gzip压缩!我只会告诉你效果适得其反,至于具体原因,还得考虑服务器压缩过程中的CPU占用还有压缩率等指标,对图片进行压缩不但会占用后台大量资源,压缩效果其实并不可观,可以说是“弊大于利”,所以请在gzip_types 把图片的相关项去掉。针对图片的相关处理,我们接下来会更加具体地介绍。

1.3 图片资源优化

  • 不要在HTML里缩放图像
  • 使用雪碧图(CSS Sprite)
  • 使用字体图标(iconfont)

1.4 使用CDN

使用CDN存放静态资源,避免带宽爆炸以及加快资源下载.

2 页面渲染性能优化

2.1 减少重绘和回流

  • CSS属性读写分离:浏览器每次对元素样式进行读操作时,都必须进行一次重新渲染(回流 + 重绘),所以我们在使用JS对元素样式进行读写操作时,最好将两者分离开,先读后写,避免出现两者交叉使用的情况。最最最客观的解决方案,就是不用JS去操作元素样式,这也是我最推荐的。
  • 通过切换class或者使用元素的style.csstext属性去批量操作元素样式。
  • DOM元素离线更新:当对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作。
  • 将没用的元素设为不可见:visibility: hidden,这样可以减小重绘的压力,必要的时候再将元素显示。
  • 压缩DOM的深度,一个渲染层内不要有过深的子元素,少用DOM完成页面样式,多使用伪元素或者box-shadow取代。
  • 图片在渲染前指定大小:因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流。
  • 对页面中可能发生大量重排重绘的元素单独触发渲染层,使用GPU分担CPU压力。(这项策略需要慎用,得着重考量以牺牲GPU占用率为代价能否换来可期的性能优化,毕竟页面中存在太多的渲染层对于GPU而言也是一种不必要的压力,通常情况下,我们会对动画元素采取硬件加速。)

2.2 减少页面重新渲染以及Dom嵌套

  • 以React为例,如果会引起页面State变化的,最好在shouldComponentUpdate进行处理,避免每次props或者state更新的时候都重新渲染,如果页面主要用来显示的话,可以使用PureComponent代替Component,注意PureComponent对于引用类型的变化,不会重新渲染。巧用Fragment代替Component,减少Dom嵌套。

3 JS阻塞性能与内存泄漏

3.1 巧用JS的防抖与节流

函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

1
2
3
4
5
6
7
8
function debounce(fn, wait) {
var timeout = null;
return function() {
if(timeout !== null)
clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

1
2
3
4
5
6
7
8
9
10
11
12
var throttle = function(func, delay) {
var prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

3.2 内存泄漏

  • 闭包内存泄漏Pattern
  • 在某个页面(SPA)WillUnMount的时候,记得关闭一些资源,例如WebSocket的断开,eChart对象置空等。

4 负载均衡

  1. 使用PM2管理多进程
  2. Nginx做反向代理
  3. Docker管理多个容器