前端性能优化一篇就够了

4,866 阅读17分钟

本文将从以下几个维度来分析优化

  • 构建优化
  • 静态资源优化
  • 网络层优化
  • 缓存
  • 渲染层优化

构建优化

1. tree shaking

构建后的js代码只包含被引用并被执行的模块,而不被引用或不被执行的模块会被删除,以起到减包的作用。

webpack 在 mode为production下默认执行tree shaking,不设置mode默认为production

注意项:tree shaking只会检测 ES2015 模块语法(即 import 和 export)。更多细节

1.1 webpack的tree shaking存在的问题

webpack的tree shaking至今实现的功能并不完美,凡是有副作用的模块,tree-shaking并不起作用

例子🌰:

// index.js
import { func2, func3 } from './module-a'
func2()
// module-a.js
import lodash from 'lodash-es'
export * from './module-b'
export const func1 = function(value) {
  return lodash.isArray(value)
}
export const func2 = function() {
  console.log('这是func2')
  return 123123
}
// module-b.js
export const func3 = () => {
  console.log('B模块的func3方法')
}

图片
通过打包的数据我们可以看到在index.js中我们并没有使用func1,但是在bundle中还是将lodash打包了进去,这就是因为func1函数的副作用,webpack自身的tree shaking并没有检测到这里有没有必要的模块。

1.2 webpack-deep-scope-plugin优化webpack的tree shaking(存在小问题)

这个插件主要用于填充webpack自身Tree-shaking的不足,通过作用域分析来消除无用的代码。原理请看

const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin')
  .default
...
plugins: [
  new WebpackDeepScopeAnalysisPlugin(),
],

图片
可以看到bundle的大小减少到了80+k,通过查看bundle的代码可以看出lodash已经被完全删除

2. css的tree shaking

对于css的tree shaking 则使用 purgecss-webpack-plugin 和 mini-css-extract-plugin 来实现

在css中添加以下代码:

.red {
  color: red
}

.blue {
  color: blue
}

然后在js文件中引用:

import './index.css'

var div = createElement('div')
div.styles.class = 'red'

console.log(div);

理想情况下,我们希望打包后的文件中只含**.red而不含未使用的.blue**,配置如下:

const TerserJSPlugin = require("terser-webpack-plugin")
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgecssPlugin = require('purgecss-webpack-plugin')
optimization: {
  // 压缩js、css
  minimizer: [
    new TerserJSPlugin({}),
    new OptimizeCSSAssetsPlugin({})
  ],
},
module: {
...
    use: [
      MiniCssExtractPlugin.loader, 
      'css-loader',
    ]
}
plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    // css tree shaking
    new PurgecssPlugin({
      paths: glob.sync(`${path.join(__dirname, '/src/**/*')}`,  { nodir: true }),
    }),
]

mini-css-extract-plugin会将css打包为一个单独的文件,这里我们使用插件purgecss-webpack-plugin帮助我们分析过滤未引用的css,最终结果如下(压缩有关的插件和配置参考第6小节):

图片

3. scope hoisting (作用域提升)

Scope Hoisting 它可以让webpack打包出来的代码文件更小,运行更快,它可以被称作为 "作用域提升"。

启用 Scope Hoisting的优点如下:

  1. 代码体积会变小,因为函数声明语句会产生大量代码。

  2. 代码在运行时因为创建的函数作用域减少了,所以内存开销就变小了。

plugins: [
  // 开启 Scope Hoisting 功能
  new webpack.optimize.ModuleConcatenationPlugin()
]

在 mode production 情况下默认开启

我们在开发环境进行验证,这里有两个文件:

// index.js
import a from './module-a'
console.log(a);
// module-a.js
export default 'module-a'

当未启动Scope Hoisting时,按照文件模块打包,生成多个函数作用域,结果如下:

图片

启用时则会将两个模块预编译到同一个模块中:

图片

4. code spiltting(代码分割)

  1. 第三方类库单独打包: 由于第三方类库的内容基本不会改变, 可以将其与业务代码分离出来, 这样就可以最大化的利用浏览器的缓存机制, 减少请求.
  2. 按需加载: Webpack支持定义分割点, 通过require.ensure/import()进行按需加载

4.1 使用entry多入口分离代码

e.g.

// index.js
import _ from 'lodash'
console.log(_.cloneDeep({a: 'index'}))
// module.js
import _ from 'lodash'
console.log(_.cloneDeep({a: 'module'}))

这里有两个文件中都引用了lodash这个第三方包,直接打包的话两个文件中都会包含lodash的代码:

图片

可以使用optimization.splitChunks来清除重复项:

optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

清除重复后会将lodash生成到一个单独的chunk中:

图片

4.2 动态导入

当涉及到动态代码拆分时,可以使用**import()**动态加载,或者webpack特有的require.ensure

// index.js
function getLodash() {
  return import(/* webpackChunkName: "lodash" */'lodash').then(({ default: _ }) => {
    return _.cloneDeep({a: 'index'})
  }).catch(e => 'error occurred')
}

getLodash().then(res => {
  console.log(res);
})

这里我们import()动态加载lodash,并且利用注释设置了chunkname,可以看到将动态引入的lodash单独拆分为一个文件:

图片

更多详细介绍可以参考Code SplittingSplitChunksPlugin

5. 优化公共资源包

将框架包 vue、react、ui框架、sdk等框架包抽离放入cdn

externals: {
 vue: "Vue",
}

6. 代码压缩

html 压缩 HtmlWebpackPlugin

js 压缩 UglifyJsPluginTerserWebpackPlugin

css 压缩 css-loader 内置了压缩,配合ExtractTextPluginMiniCssExtractPlugin

给css加上hash

6.1 html压缩

e.g.

const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
  new HtmlWebpackPlugin({
    minify: {
      collapseWhitespace: true,
      removeComments: true,
    },
  }),
]

可以看到html文件实现了压缩:

图片

HtmlWebpackPlugin 3.x 中,minify为true时默认为{},此时需要手动配置,在4.x中则为

{
  collapseWhitespace: true,
  removeComments: true,
  removeRedundantAttributes: true,
  removeScriptTypeAttributes: true,
  removeStyleLinkTypeAttributes: true,
  useShortDoctype: true
}

详细参见:github.com/jantimon/ht… 关于具体配置项可以参考html-minifier

6.2 js压缩

webpack 4.x 中默认使用TerserWebpackPlugin进行js压缩,原有的UglifyjsWebpackPlugin不推荐使用

关于TerserWebpackPlugin和UglifyJsPlugin,你需要知道以下几点:

  • TerserWebpackPlugin底层使用的是terser
  • UglifyJsPlugin底层使用的是uglify-js
  • uglify-js 不支持es6,需要另外配置 uglify-es,但是uglify-es不再维护更新
  • terser 则保留了uglify-js@3和uglify-es的api和兼容性

在3.x中开发环境下配置如下:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
plugins: [
  new UglifyJsPlugin(),
]

4.x开发环境中开启,production下minimize默认为true:

optimization: {
  minimize: true,
}

压缩前:

图片

压缩后:

图片

还可以在minimizer中新增TerserWebpackPlugin实例覆盖默认压缩:

  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        test: /\.js(\?.*)?$/i,
      }),
    ],
  },

更多配置项

6.3 压缩css

未来在webpack5中将会引入内置的css minimizer,在4.x中需要手动配置 optimize-css-assets-webpack-plugin

手动配置 minimizer 会覆盖掉默认配置,因此需要补上TerserJSPlugin压缩js

MiniCssExtractPlugin则会提取css到单独的文件,配置如下:

const TerserPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
optimization: {
  minimizer: [
    // 压缩js
    new TerserJSPlugin({}),
    // 压缩css
    new OptimizeCSSAssetsPlugin({})
  ]
},
plugins: [
  new MiniCssExtractPlugin({
    filename: "[name].css",
    chunkFilename: "[id].css"
  })
],
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        "css-loader"
      ]
    }
  ]
}

7. 使用preload与prefetch

在import动态加载中配置preload和prefetch

预加载:

是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。
import(/* webpackPreload: true */ 'Modal');

预取:

是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。
import(/* webpackPrefetch: true */ 'Modal');

二者的区别:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

preload:

图片

prefetch:

图片

更多资料

8. chunk名称

将hash替换为chunkhash,这样当chunk不变时,缓存依然有效

使用Name而不是id

每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变

webpack缓存

静态资源优化

1.Gzip -> Brotli

Brotli 压缩算法具有多个特点,最典型的是以下 2 个:

  • 针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%;
  • 当 Brotli 压缩级别为 1 时,压缩率比 Gzip 压缩等级为 9(最高)时还要高;

图片

brotil的支持情况 可查看caniuse

1.1 不压缩的情况

图片

1.2 gzip 9级压缩

图片

1.3 brotil 11级压缩

图片

1.4 选择合适的压缩时机

动态压缩即时发生。用户发出请求,压缩内容(当用户等待时)并且提供压缩内容。

静态压缩在用户请求之前在磁盘上压缩资产的时间。当用户请求资产时,不会发生压缩。预压缩资产只是从磁盘提供。

webpack提供的压缩方式compression-webpack-pluginbrotli-webpack-plugin

gzip:

const CompressionWebpackPlugin = require('compression-webpack-plugin')
new CompressionWebpackPlugin({
  asset: '[path].gz[query]',
  algorithm: 'gzip',
  test: /\.(js|css|html|svg)$/,
  threshold: 10240,
  minRatio: 0.8
})

brotli:

const BrotliPlugin = require('brotli-webpack-plugin')
new BrotliPlugin({
  asset: '[path].br[query]',
  test: /\.(js|css|html|svg)$/,
  threshold: 10240,
  minRatio: 0.8
})

1.5 nginx.conf

gzip  on;
gzip_vary               on;
gzip_min_length         1024;
gzip_buffers            128 32k;
gzip_comp_level         9;
gzip_http_version       1.1;
gzip_proxied            expired no-cache no-store private auth;
gzip_types              text/plain text/css text/xml application/xml application/json text/javascript application/javascript application/x-javascript;

brotli on;
brotli_types text/plain text/css text/xml application/xml application/json text/javascript application/javascript application/x-javascript;
brotli_static off;
brotli_comp_level 11;
brotli_buffers 16 8k;
brotli_window 512k;
brotli_min_length 20;

2.图片优化

2.1 选择合适的图片格式

图片

2.2 图片压缩

压缩工具:

也可以通过webpack压缩 imagemin-webpack-plugin

tinypng

图片

imagemin

依赖:url-loader、file-loader、imagemin-webpack-plugin

图片

const path = require('path')
const ImageminPlugin = require('imagemin-webpack-plugin').default
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              name: '[name].[hash:8].[ext]',
              outputPath: 'imgs/',
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ImageminPlugin({ test: /\.(jpe?g|png|gif|svg)$/i }),
  ],
}

不推荐在打包的时候通过webpack压缩,因为这样会大量增加打包的时间,并且imagemin这个插件的压缩能力远不如tinypng

2.3 大的背景图采用图片切分(h2)

将过大的背景图切分成多张小图,通过html的方式拼到一起。

利用浏览器的并发加载(h2)

2.4 小图base64/字体/雪碧图

网络请求 缓存 灵活性 其他
雪碧图 1.放大失真 2.同一图标,不同颜色,会有重复部分 3.只用几张也需要加载整张图
iconfont 1.矢量放大不失真 2.跟普通字体无异,可以设置size,color,opacity, 等 3.文件小
base64 1.增加额外的css、js大小 2.增加图片资源的大小

2.5 响应式图片

图片

响应式图片不仅仅指图片的排版和布局,还包括根据设备大小加载不同的图片。

image:

通过设置image标签上的srcset告知浏览器在不同屏幕宽度下加载不同的图片

通过设置image标签上的sizes来设置图片的尺寸临界点,明确定义了图片在不同的media conditions下应该显示的尺寸。

picture:

浏览器会遍历 中的,直到找到一个满足当前环境,然后将该中的srcset设置到

响应式图片断点生成器

2.5 使用体积小、可缓存的favicon.ico

favicon.ico一般存放在网站根目录下,无论是否在页面中设置,浏览器都会尝试请求这个文件。

所以确保这个图标:

  • 存在(避免 404);
  • 尽量小,最好小于 1K;
  • 设置较长的过期时间。
    图片

3.使用cdn

在不同地域的用户请求资源(访问网站)的响应速度具有很大的差异,为了提高用户体验,我们在用户和服务器中间加了一层,就是CDN。CDN(Content Delivery Network),它的思想就是将源站的内容分发到最接近用户的网络边缘节点,让用户能够就近取得所需的内容,提高用户访问的响应速度。

当用户发起HTTP请求时,通过CDN向边缘节点服务器发起请求,边缘节点会检测当前节点是否具有你想请求的数据,如果没有就去源站,如果有请求数据就会进一步判断,这个数据是否在有效期,根据是否过期来决定。

网络层优化

一次完整的http请求过程

DNS解析(T1) -> 建立TCP连接(T2) -> 发送请求(T3) -> 等待服务器返回首字节(TTFB)(T4) ->> 接收数据(T5)

图片

  • Queueing:请求排队。
  • Stalled:请求阻塞;
  • Proxy negotiation: 与代理服务器连接的时间花费
  • DNS Lookup:dns查询。
  • Initial connection:建立TCP连接的时间,就相当于客户端从发请求开始到TCP握手结束这一段。
  • SSL(包含于HTTPS连接中):完成SSL握手的时间花费。
  • Request sent(发送请求):发送HTTP请求的时间(从第一个字节发出前到最后一个字节发出后的时间)
  • Waiting(TTFB) :请求发出后,到收到响应的第一个字节所花费的时间(Time To First Byte),发送请求完毕到接收请求开始的时间;通常是耗费时间最长的。从发送请求到收到服务器响应的第一字节之间的时间,受到线路、服务器距离等因素的影响。
  • Content Download(下载):收到响应的第一个字节,到接受完最后一个字节的时间,就是下载时间。

影响一个http请求的主要因素

  • 带宽 - 网速
  • 延迟
    • 浏览器阻塞: 以chrome为例,浏览器对于同一个域名,同时只能有6个连接,超过这个数的请求就会被阻塞
    • DNS查询:浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用DNS缓存结果来达到减少这个时间的目的。
    • TCP连接:HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

1. 减少阻塞时间

1.1 可能产生阻塞的原因

  • There are higher priority requests.
  • There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
  • The browser is briefly allocating space in the disk cache

1.2 合理的请求合并与拆分(http1.1)

图片

以chrome为例,http1.1的并发请求数量最大是6(同一域名下),超出的并发请求必须等待

**对于大资源:**是否合并对于加载时间没有明显影响,但拆分资源可以更好的利用浏览器缓存,不会因为某个资源的更新导致所有资源缓存失效,而资源合并后,任一资源的更新都会导致整体资源的缓存失效。另外还可以利用域名分片技术,将资源拆分部署到不同域名下,既可以分散服务器的压力,又可以降低网络抖动带来的影响。

**对于小资源:**合并资源往往具有更快的加载速度,但在网络带宽状况良好的情况下,因为提升的时间单位以ms计量,收益可以忽略。如果网络延迟很大,服务器响应速度又慢,则可以带来一定收益,但在高延迟的网络场景下,又要注意合并资源后可能带来网络往返次数的增加,进而影响到加载时间。

1.3 升级到http2

2. 减少DNS查询时间

2.1 dns查询的步骤

浏览器对网站第一次的域名DNS解析查找流程依次为:浏览器缓存 -> 系统缓存 -> 本地hosts文件 -> 路由器缓存 -> ISP DNS缓存 -> 递归搜索

2.2 服务器增加DNS缓存

现在一般服务器都具备DNS缓存

2.3 dns prefetch

  1. 启用DNS预解析:

在浏览器支持 DNS 预解析的特性时即使不使用该标签浏览器依然会进行预解析。

<meta http-equiv="x-dns-prefetch-control" content="on"> // 关闭 off
  1. 强制查询特定的主机名:
<link rel="dns-prefetch" href="//domain.com">

图片

淘宝的预解析

注:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数。

需要注意的是,虽然使用 DNS Prefetch 能够加快页面的解析速度,但是也不能滥用,因为有开发者指出 禁用DNS 预读取能节省每月100亿的DNS查询 。

2.4 减少dns查询

  1. 利用Connection:keep-alive特性建立持久连接,可以在当前连接上进行多个请求,无需再进行域名解析
  2. 资源放在同一域名下,利用dns的缓存

3. 减少TCP连接时间

  1. 建立持久连接(keep-alive)
  2. 开启OCSP(在线证书状态协议)
  3. 升级到http2

4. 减少Request请求时间

4.1 减少cookie的使用

每一次的http请求都会默认携带上cookie,cookie过大的话会导致传输变慢

4.2 cookie隔离

对于一些静态资源(图片)的获取,是不需要用到cookie,所以尽量图片资源放入无cookie服务器,减少与主域名之间的cookie混用

5. 减少TTFB时间

  • cdn
  • 服务器性能
  • ...

6. 减少下载时间

  • 减少response data
  • 利用缓存

7. 升级到http2

http1.1与http2的通道模型

图片

图片

http2相比http1.1的优点

  • 二进制协议

http2采用二进制的传输协议,而http1.1采用的是文本传输,二进制格式的传输效率要比文本快的多。

  • 多路复用

同一个TCP连接中可以同时发送多个请求而不会阻塞

  • 头部压缩

对消息头采用Hpack进行压缩传输,能够节省消息头占用的网络流量,http1.1每次请求,都会携带大量冗余的头信息,浪费了很多宽带资源。

  • 服务端push

它允许 Web 服务器在收到浏览器的请求之前提前发送一些资源给客户端

  • http2同时向下兼容http1.x版本

图片

http2.akamai.com/demo

8. 其他

8.1 避免空的src,href

// html
<img src="" />
// js
var img = new Image(); 
img.src = "";

虽然src属性为空字符串,但浏览器仍然会向服务器发起一个HTTP请求:

  • IE 向页面所在的目录发送请求;
  • Safari、Chrome、Firefox向页面本身发送请求;
  • Opera不执行任何操作。

空src产生请求的后果不容小觑:

  • 给服务器造成意外的流量负担,尤其时日 PV 较大时;
  • 浪费服务器计算资源;
  • 可能产生报错。

空的href属性也存在类似问题。用户点击空链接时,浏览器也会向服务器发送HTTP请求,可以通过JavaScript阻止空链接的默认的行为。

8.2 减少重定向

每一次的重定向都是需要重新再发起一次http请求

8.3 避免404

HTTP请求很昂贵,返回无效的响应(如404未找到)完全没必要,降低用户体验而且毫无益处。 一些网站设计很酷炫、有提示信息的404页面,有助于提高用户体验,但还是浪费服务器资源。尤其糟糕的是外部脚本返回404,不仅阻塞其他资源下载,浏览器还会尝试把404页面内容当作JavaScript解析,消耗更多资源。

8.4 IPv4升级到IPv6

因为IPv4即将用完以及主要的移动网络正在迅速采用IPv6(美国已经达到50% 的 IPv6 使用阈值),将你的 DNS 更新到 IPv6 以应对未来是一个好的想法。只要确保在网络上提供双栈支持,就可以让 IPv6 和 IPv4 同时运行。毕竟,IPv6 不是向后兼容的。研究显示,也是正因为 IPv6 自带 NDP 以及路由优化,所以才能够让网站的载入速度提升10%到15%。

缓存

1. http缓存

当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本并且没有失效,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。

常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指GET请求。

http缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,命中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否命中协商缓存,命中则返回304,否则服务器会返回新的资源。

1.1 强缓存

强制缓存在缓存数据未失效的情况下(即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,http状态码为200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以Ctrl + F5一顿操作之后就好了。 跟强制缓存相关的header头属性有(Pragma(http1.0)/Cache-Control(http1.1)/Expires)

图片

1.2 协商缓存

当第一次请求时服务器返回的响应头中没有Cache-Control和Expires或者Cache-Control和Expires过期还或者它的属性设置为no-cache时(即不走强缓存),那么浏览器第二次请求时就会与服务器进行协商,与服务器端对比判断资源是否进行了修改更新。如果服务器端的资源没有修改,那么就会返回304状态码,告诉浏览器可以使用缓存中的数据,这样就减少了服务器的数据传输压力。如果数据有更新就会返回200状态码,服务器就会返回更新后的资源并且将缓存信息一起返回。跟协商缓存相关的header头属性有(ETag/If-Not-Match 、Last-Modified/If-Modified-Since)请求头和响应头需要成对出现

图片

协商缓存的执行流程是这样的:当浏览器第一次向服务器发送请求时,会在响应头中返回协商缓存的头属性:ETag和Last-Modified,其中ETag返回的是一个hash值,Last-Modified返回的是GMT格式的最后修改时间。然后浏览器在第二次发送请求的时候,会在请求头中带上与ETag对应的If-Not-Match,其值就是响应头中返回的ETag的值,Last-Modified对应的If-Modified-Since。服务器在接收到这两个参数后会做比较,如果返回的是304状态码,则说明请求的资源没有修改,浏览器可以直接在缓存中取数据,否则,服务器会直接返回数据。

1.3 总结

图片

图片

图片

2. 浏览器缓存

  • Storage
    • LocalStorage
    • SessionStorage
    • IndexedDB
    • Web SQL
    • Cookies
  • Cache
    • Cache Storage - service worker
    • Application Cache - 离线缓存(浏览器支持率太低)

渲染层优化

1. 防止阻塞渲染

  • css放在header提前加载
  • js文件放在底部,防止阻塞解析
  • 一些不改变dom和css的js使用defer和async属性告诉浏览器可以异步加载,不阻塞解析

2. 减少重绘和回流

实际开发中不可必变产生重绘和回流,我们只能做到尽可能的减少这种行为的发生

  • 减少dom操作
  • 优化dom结构,将可能产生回流的元素,使用脱离文档流的方式布局。
  • img标签设置高度
  • dom操作离线操作(display:none),只触发一次回流
  • 使用transform来做变形和位移,不会造成回流

3. 服务端渲染

next,nuxt,等

首页骨架屏

4. 提高代码质量

html:

  • 优化dom的层级结构,太深了会增加dom树的构建时间,对js查找深层节点也会造成很大的负担
  • meta标签里增加对文档的编码定义,便于浏览器解析
  • 不要在HTML中缩放图片

css:

  • 减少css嵌套层级,选择合适的选择器
  • 对于首屏的关键css可以使用style标签内联
  • 避免使用@import
  • 避免使用css表达式
  • 避免使用滤镜
  • 动画渲染使用3d语法,开启GPU加速

css选择器的效率排序:

1.id选择器(#myid) 2.类选择器(.myclassname) 3.标签选择器(div,h1,p) 4.相邻选择器(h1+p) 5.子选择器(ul < li) 6.后代选择器(li a) 7.通配符选择器(*) 8.属性选择器(a[rel="external"]) 9.伪类选择器(a:hover, li:nth-child)

js:

  • 不要频繁操作dom,可以放在requestAnimationFrame中执行
  • 减少通过js直接修改元素的样式,可以通过修改class名的方式统一修改
  • 需要多次访问的dom节点,需要通过变量转存,以免重复访问dom节点造成性能损耗
  • 及时清理不用的定时器
  • 对于高频触发的事件增加防抖(debounce)、节流(throttle)
  • 图片懒加载、预加载、默认图

速效方案

  1. js/css tree shaking
  2. 合理的代码分割和合并
  3. 代码压缩,提前gzip或brotli减少服务器动态压缩的时间
  4. 选择合适格式的图片并压缩
  5. http协议升级至http2
  6. 静态资源使用cdn

本文参考

Front-End Performance Checklist 2019 [PDF, Apple Pages, MS Word]

体积减少80%!释放webpack tree-shaking的真正潜力

为什么要用自适应的图片

HTTP请求合并 vs HTTP并行请求

X-DNS-Prefetch-Control

前端性能优化之雅虎35条军规