前端性能优化

232 阅读13分钟

首次加载 & 非首次加载优化

事件代理

Css

  • 层级尽量扁平,避免嵌套过多层级的选择器;
  • 使用特定的选择器,避免解析器过多层级的查找;
  • 减少使用通配符与属性选择器;
  • 减少不必要的多余属性;
  • 避免使用!important标识,可以选择其他选择器;
  • 实现动画时优先使用CSS3的动画属性,动画时脱离文档流,开启硬件加速;
  • 使用link标签代替@import;
  • 将渲染首屏内容所需的关键CSS内联到HTML中;
  • 使用资源预加载指令preload让浏览器提前加载CSS资源并缓存;
  • 使用Gulp,Webpack等构建工具对CSS文件进行压缩处理;

回流 重绘优化

css

  • 避免使用 table 布局。
  • 尽可能在 DOM 树的最末端改变 class。
  • 避免设置多层内联样式。
  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
  • 避免使用 CSS 表达式(例如:calc())。

Javascript

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

Webpack

webpack 的优化瓶颈,主要是两个方面:

webpack 的构建过程太花时间 webpack 打包的结果体积太大

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
loader: 'babel-loader?cacheDirectory=true'

开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍

打包体积

为了减小包的打包体积,可以从以下几个方面进行优化:

  • 提取第三方库或通过引用外部文件的方式引入第三方库
  • 代码压缩插件UglifyJsPlugin
  • 服务器启用gzip压缩
  • 按需加载资源文件 require.ensure
  • 优化devtool中的source-map
  • 剥离css文件,单独打包
  • 去除不必要插件,通常就是开发环境与生产环境用同一套配置文件导致

打包效率

  • 开发环境采用增量构建,启用热更新
  • 开发环境不做无意义的工作如提取css计算文件hash等
  • 配置devtool
  • 选择合适的loader,个别loader开启cache 如babel-loader
  • 第三方库采用引入方式
  • 提取公共代码
  • 优化构建时的搜索路径 指明需要构建目录及不需要构建目录
  • 模块化引入需要的部分

webpack的打包速度?

  • loader:使用include和exclude指定搜索文件的范围。
  • babel-loader:配置后面加上loader:'babel-loader?cacheDirectory=true'将编译过的文件缓存起来,下次只需要编译更改过的代码文件即可。
  • HappyPack:使用这个插件开启loader多线程打包。
  • DllPlugin:将特定的类库提前打包然后引入,减少打包类库的次数,只有当类库更新版本才进行重新打包。
  • resolve.alias:配置别名,更快找到路径。
  • module.noParse:确定这个文件没有其他依赖时,让webpack打包时不扫描该文件。

减小webpack的打包后的体积?

  • Scope Hoisting:分析模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数里。
  • Tree Shaking:删除项目中未被引用的代码。

不要放过第三方库

第三方库以 node_modules 为代表,它们庞大得可怕,却又不可或缺。

处理第三方库的姿势有很多,其中,Externals 不够聪明,一些情况下会引发重复打包的问题;而 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;出于对效率的考虑,我们这里为大家推荐 DllPlugin。

DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包。

用 DllPlugin 处理文件,要分两步走:

基于 dll 专属的配置文件,打包 dll 库 基于 webpack.config.js 文件,打包业务代码 以一个基于 React 的简单项目为例,我们的 dll 的配置文件可以编写如下:

const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // 依赖的库数组
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // DllPlugin的name属性需要和libary保持一致
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // context需要和webpack.config.js保持一致
        context: __dirname,
      }),
    ],
}

随后,我们只需在 webpack.config.js 里针对 dll 稍作配置:

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // 编译入口
  entry: {
    main: './src/index.js'
  },
  // 目标文件
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相关配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是我们第一步中打包出来的json文件
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

一次基于 dll 的 webpack 构建过程优化,便大功告成了!

Happypack

webpack 是单线程的, 试着用第三方插件提高为多线程。

不同业务场景下的图片方案

Web 图片格式有 JPEG/JPG、PNG、WebP、Base64、SVG 等

待补充。。。

代理服务器缓存机制???

浏览器缓存机制

浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  • 1.Memory Cache
  • 2.Service Worker Cache
  • 3.HTTP Cache
  • 4.Push Cache (http2)

Http缓存

  • 强缓存
  • 协商缓存

public 与 private

public 与 private 是针对资源是否能够被代理服务缓存而存在的一组对立概念。

如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了 private,则该资源只能被浏览器缓存。private 为默认值。

no-store与no-cache

no-cache 绕开了浏览器:我们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期(即走我们下文即将讲解的协商缓存的路线)。

no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。

Etag 是由服务器为每个资源生成的唯一的标识字符串

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。

本地存储

  • Cookie
  • Web Storage (Local Storage & Session Storage)
  • IndexedDB (运行在浏览器上的非关系型数据库)

CDN

  • 缓存
  • 回源

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。

“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。

事件的节流(throttle)与防抖(debounce)

  • p.proto
  • p.constructor.prototype
  • Object.getPrototypeOf(p)

原型链上的查找

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。下面给出一个具体的例子来说明它:

console.log(g.hasOwnProperty('vertices'));
// true

console.log(g.hasOwnProperty('nope'));
// false

console.log(g.hasOwnProperty('addVertex'));
// false

console.log(g.__proto__.hasOwnProperty('addVertex'));
// true

hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys())

注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

性能优化方面

webpack打包 压缩文件

渲染优化方案

1、优化渲染关键路径方案

  通过优化渲染关键路径,可以优化页面渲染性能,减少页面白屏时间。

  • 优化JS:JavaScript文件加载会阻塞DOM树的构建,可以给
  • 优化CSS:浏览器每次遇到标签时,浏览器就需要向服务器发出请求获得CSS文件,然后才继续构建DOM树和CSSOM树,可以合并所有CSS成一个文件,减少HTTP请求,减少关键资源往返加载的时间,优化渲染速度。

2、其他优化方案

  • 加载部分HTML 浏览器先加载主要HTML初始化静态部分,动态变化的HTML内容通过Ajax请求加载。这样可以减少浏览器构建DOM树的工作量,让用户感觉页面加载速度很快。

  • 压缩 对HTML、CSS、JavaScript这些文件去除冗余字符(例如不必要的注释、空格符和换行符等),再进行压缩,减小文件数据大小,加快浏览器解析文件编码。

  • 图片加载优化 1)小图标合并成雪碧图,进而减少img的HTTP请求次数; 2)图片加载较多时,采用懒加载的方案,用户滚动页面可视区时再加载渲染图片。

  • HTTP缓存 浏览器自带了HTTP缓存的功能,只需要确保每个服务器响应的头部都包含了以下的属性:

  • 1)ETag: ETag是一个传递验证令牌,它对资源的更新进行检查,如果资源未发生变化时不会传送任何数据。当浏览器发送一个请求时,会把ETag一起发送到服务器,服务器会根据当前资源核对令牌(ETag通常是对内容进行Hash后得出的一个指纹),如果资源未发生变化,服务器将返回304 Not Modified响应,这时浏览器不必再次下载资源,而是继续复用缓存。

  • 2)Cache-Control: Cache-Control定义了缓存的策略,它规定在什么条件下可以缓存响应以及可以缓存多久。

    • a、no-cache: no-cache表示必须先与服务器确认返回的响应是否发生了变化,然后才能使用该响应来满足后续对同一网址的请求(每次都会根据ETag对服务器发送请求来确认变化,如果未发生变化,浏览器不会下载资源)。no-store直接禁止浏览器以及所有中间缓存存储任何版本的返回响应。简单的说,该策略会禁止任何缓存,每次发送请求时,都会完整地下载服务器的响应。
    • b、public private: 如果响应被标记为public,则即使它有关联的HTTP身份验证,甚至响应状态代码通常无法缓存,浏览器也可以缓存响应。如果响应被标记为private,那么这个响应通常只为单个用户缓存,因此不允许任何中间缓存(CDN)对其进行缓存,private一般用在缓存用户私人信息页面。
    • c、max-age: max-age定义了从请求时间开始,缓存的最长时间,单位为秒。 占位 image.png
  • 减少DOM操作

    • 最小化DOM访问次数,尽量缓存访问DOM的样式信息,避免过度触发回流。
    • 如果在一个局部方法中需要多次访问同一个dom,则先暂存它的引用。
  • 采用更优的API替代消费高的api,转换优化消费高的集合

    • 用querySelectorAll()替代getElementByXX()。
    • 开启动画的GPU加速,把渲染计算交给GPU。
    • 少用HTML集合(类数组)来遍历,因为集合遍历比真数组遍历耗费更高。
    • 用事件委托来减少事件处理器的数量。
  • 减少重排

    • 避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性
    • 实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局
    • 动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
    • 不要使用table布局,因为table中某个元素旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围
  • css及动画处理

    • 少用css表达式
    • 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
    • 动画尽量使用在绝对定位或固定定位的元素上;
    • 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
  • HTML文档结构层次尽量少,最好不深于六层

  • JS 脚本尽量后放

  • 样式结构层次尽量简单

  • 少量首屏样式使用内联方式放在标签内

  • 在脚本中尽量减少DOM操作,尽量访问离线DOM样式信息,避免过度触发回流

  • 减少通过 JS 代码修改元素样式,尽量使用修改 class 名方式操作样式或动画

  • 尽量减少浏览器重排和重绘的一些情况发生

  • 2020年了!就不要使用 table 布局了

  • CSS 动画中尽量只使用 transform 和 opacity ,不会发生重排和重绘

  • 隐藏在屏幕外,或在页面滚动时,尽量停止动画

  • 尽可能只使用 CSS 做动画,CSS动画肯定比 JS 动画要好很多

  • 避免浏览器的隐式合成

  • 改变复合层的尺寸

Script 放在底部 且 async or defer

缓存

代码方面

a.b.c.d

a.b.c.d 和 a['b']['c']['d'],哪个性能更高?

github.com/Advanced-Fr…

for & foreach

github.com/Advanced-Fr…

call比apply的性能要好

github.com/Advanced-Fr… github.com/noneven/__/…

尾调用 尾递归(内存溢出)

es6.ruanyifeng.com/#docs/funct… 注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。

闭包

小心内存溢出

http

持久连接 & 管线化