2023 前端性能优化清单

45,591 阅读13分钟

前言 🎉

本文将从三个角度页面渲染,打包优化,总体优化)分析前端性能优化的一些方案。如有错误,欢迎指出,也可以在评论区提出你对性能优化的一些见解,相互学习,共同进步!💪

ps: 阿彬之前也总结过一篇关于性能检测的文章,有兴趣的可以看看提点建议传送门

一、页面渲染 🌈

1. 减少页面重绘(repaint)和回流(reflow)

回流:计算可见节点的位置和几何信息;
重绘:重新绘制节点,将渲染树的每个节点都转换为屏幕上的实际像素。

  • 🍀 减少使用 css 属性简写,如:用border-width, border-style, border-color代替border。因为css简写把所有值初始化为initial,尽量不使用属性简写可以最小化重绘(repaint)和回流(reflow),(实际工作中,由于css简写带来的性能影响微乎其微,再加上css全都展开写还会增加code size,另外css简写还能解决一些样式覆盖的问题,写起来也比较简单快捷,所以实际工作个人怎么顺手怎么来就好~)

  • 🍀 在 GPU 上渲染动画,浏览器针对处理 CSS 动画进行了优化,可以很好地处理触发重排(因此也导致重绘)的动画属性。为了提高性能,可以将设置了动画的节点从主线程移到 GPU 上。 可以触发 GPU 硬件加速的css属性有:transform、filter、will-change、position: fixed等,只要元素被提升为图层,动画就会在 GPU 中完成,从而改善性能,尤其是在移动设备上(但是不能滥用,会导致性能问题)。

  • 🍀 使用css属性 will-change 提高性能,它的作用是告诉浏览器元素的哪些属性需要修改,使浏览器能够在元素实际更改之前设置优化,通过在实际更改前执行耗时的工作以提升性能(will-change 同样不能滥用,一半在有很大计算量的动画出现性能问题时考虑用 will-change 提高性能)。

  • 🍀 通过修改className批量修改元素样式;

  • 🍀 复杂的动画元素定位要设置为 fixedabsoult,避免引起回流;

  • 🍀 不使用table布局(table元素一旦触发回流就会导致table里所有的其它元素回流);

  • 🍀 DOM 元素上下移动用用translate替代top修改;

  • 🍀 需要创建多个DOM节点时,使用DocumentFragment一次性创建。

  • 🍀 元素适当地定义高度或最小高度,否则元素的动态内容载入时,会出现页面元素的晃动或位置,造成回流(比如图片要定义宽高,避免页面塌陷,同时减少回流);

  • 🍀 减少使用层级较深的选择器,或其他一些复杂的选择器,以提高CSS渲染效率;

  • 🍀 在大量修改元素样式时,可以先用display: none将其隐藏,修改完再设置为display: block,这样只会造成两次回流;

  • 🍀 必要时使用css属性 contain 将一个元素及其内容与文档流隔离,防止元素内部在其包围盒外产生副作用。

2. 图片压缩,图片分割,精灵图

  • 🍀 图片压缩:开发中比较重要的一个环节,现在很多图床工具都自带压缩功能的。如果需要自己压缩的话,可以在tinyPng 网站压缩一下,一看就会;

  • 🍀 图片分割:如果页面需要加载一张效果图,比如真机渲染图,UI设计师不允许压缩的时候,我们就可以将图片分割,然后再用css布局将图片拼接到一起。

  • 🍀 精灵图:与图片分割相反,精灵图是将很多小图片合并到一张大图里,这样加载页面的时候,只需要加载一张图片,就可以加载出来所有的页面元素。这样在一定程度上提高了页面加载速度。而我们使用精灵图的方式,也是通过定位,即通过background-position来移动背景图,从而显示出我们想要显示出来的部分。
    但是精灵兔有个很大的不足,那就是牵一发而动全身,因为我们使用精灵图需要准确测量每个元素的位置,一旦要调整页面,将会是一项很麻烦的工作。

3. 字体包压缩

  • 🍀 问题描述: 在做类似活动h5的时候,难免需要使用 @font-face 接口引入一些字体包,以达到丰富的页面效果。但是完整的字体包文件一般都很好几兆,加载页面时,不仅会阻塞页面渲染,还会导致文字开始不会显示,直至字体包加载完才会显示文字。

  • 🍀 解决方式: 可以使用font-spider字蛛将要使用到的文字提取出来。

4. 懒加载/预加载资源

  • 🍀 懒加载: 简言之就是只有当图片出现在浏览器的可视区域内时,才加载图片让图片显示出来(在此之前可以将所有图片元素的路径全都统一设置成一张1*1px的占位图)。
    判断图片出现在浏览器可视区域的方法:图片距离顶部的高度(offsetTop) - 页面被卷去的高度(scrollTop) 〈= 浏览器的可视区域的高度(innerHeight)

  • 🍀 预加载: Resource Hints(资源预加载)包括预连接、资源与获取、资源预渲染等。
    预加载的思路有如下两个:
    当前将要获取资源的列表;
    通过当前页面或应用的状态、用户历史行为或 session 预测用户行为及必需的资源.
    实现Resource Hints的方法有很多种,可分为基于 link 标签的 DNS-prefetch、subresource、preload、 prefetch、preconnect、prerender,和本地存储 localStorage。详情可查看: 预加载5种不同的方法3种Javascript图片预加载的方法详解

二、打包优化 🌞

1. webpack优化resolve.alias配置(vite同理)

resolve.alias 配置通过别名来将原导入路径映射成一个新的导入路径。
可以起到两个作用:1.起别名;2.减少查找过程。
例如:

resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },

2. webpack优化resolve.extensions配置(vite同理)

resolve.extensions 代表后缀尝试列表,它也会影响构建的性能,默认是:
extensions: ['.js', '.json']
例如遇到require('./data')这样的导入语句时,Webpack会先去寻找./data.js文件,如果该文件不存在就去寻找./data.json文件,如果还是找不到就报错。
【所以后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中,频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。】
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把require('./data')写成require('./data.json')。

resolve: {
    extensions: ['.js', '.vue', '.json'],
}

3. webpack缩小loader范围

loader是很消耗性能的一个点,我们在配置loader的时候,可以使用include和except来缩小loader执行范围,从而优化性能。 例如:

{
    test: /\.svg$/,
    loader: 'svg-sprite-loader',
    include: [resolve('src/icons')]
 },

4. split chunks代码分包

在没配置任何东西的情况下,webpack 4 就智能的帮你做了代码分包。入口文件依赖的文件都被打包进了main.js,那些大于 30kb 的第三方包,如:echarts、xlsx、dropzone等都被单独打包成了一个个独立 bundle。 其它被我们设置了异步加载的页面或者组件变成了一个个chunk,也就是被打包成独立的bundle。 它内置的代码分割策略是这样的:

新的 chunk 是否被共享或者是来自 node_modules 的模块;
新的 chunk 体积在压缩之前是否大于 30kb;
按需加载 chunk 的并发请求数量小于等于 5 个;
页面初始加载时的并发请求数量小于等于 3 个;

大家可以根据自己的项目环境来更改配置。webpack配置代码如下:

splitChunks({
  cacheGroups: {
    vendors: {
      name: `chunk-vendors`,
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
      chunks: 'initial',
    },
    dll: {
      name: `chunk-dll`,
      test: /[\\/]bizcharts|[\\/]\@antv[\\/]data-set/,
      priority: 15,
      chunks: 'all',
      reuseExistingChunk: true
    },
    common: {
      name: `chunk-common`,
      minChunks: 2,
      priority: -20,
      chunks: 'all',
      reuseExistingChunk: true
    },
  }
})

没有使用webpack4.x版本的项目,可以通过按需加载的形式进行分包:
webpack如何使用按需加载

vite配置代码如下:

 build: {
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',  // 引入文件名的名称
        entryFileNames: 'js/[name]-[hash].js',  // 包的入口文件名称
        assetFileNames: '[ext]/[name]-[hash].[ext]' // 资源文件像 字体,图片等
      }
    }
 }

5. tree shaking

tree shaking(摇树)。望文生义,它是用来清除我们项目中的一些无用代码,它依赖于ES中的模块语法得以实现。 比如日常使用lodash的时候:

import _ from 'lodash'

如果如上引用lodash库,在构建包的时候是会把整个lodash包打入到我们的bundle包中的。

import _isEmpty from 'lodash/isEmpty';

如果如上引用lodash库,在构建包的时候只会把isEmpty这个方法抽离出来再打入到我们的bundle包中。

tree shaking可以大大减少包体积,是性能优化中的重要一环。 在 vite 和 webpack4.x 中都已经默认开启tree-shaking。

6. vite关闭一些打包配置项()

webpack也有类似的配置,自行查阅

build: { 
      terserOptions: {
        compress: {
          //生产环境时移除console
          drop_console: true,
          drop_debugger: true,
        },
      },
      //关闭文件计算
      reportCompressedSize: false,
      //关闭生成map文件 可以达到缩小打包体积
      sourcemap: false, // 这个生产环境一定要关闭,不然打包的产物会很大
}

三、总体优化 🌻

1. SSR服务端渲染

SSR(Server Side Rendering),即服务端渲染。它指的是渲染过程在服务端完成,最终的渲染结果 HTML 页面通过 HTTP 协议发送给客户端,又叫“同构“。

SSR主要带来的好处就是 SEO 和首屏加载速度大大提高。
目前流行的 Vue/React 前端框架,都已经推出了SSR的解决方案:
Vue的nuxt.js
React的next.js
以前的一篇文章现有Vue项目改造SSR(简单上手)

2. 开启 gzip 压缩

Gzip 对文件进行压缩,能大大提高首屏加载速度,对于纯文本文件我们可以至少压缩到原大小的40%。注意:图片最好不要进行gzip压缩!

image.png

为了减轻服务器压力,我们可以在打包时就生成gzip压缩文件,具体操作如下:

第一步,在vue项目中安装依赖并将productionGzip改为true,开启Gzip压缩: npm install --save-dev compression-webpack-plugin 在这里插入图片描述 第二步,运行 npm run build打包项目,这时可能会报错,提示ValidationError: Compression Plugin Invalid Options。根据官网提示,需要将CompressionWebpackPlugin的设置由asset改为filename。 在这里插入图片描述 在这里插入图片描述 第三步,npm run build打包项目,生成.gz文件即为成功

3. Brotli 算法压缩

Brotli 压缩算法 是 Google 2015年推出的无损压缩算法。

  • 启用 Brotli 压缩算法,对比 Gzip 压缩 CDN 流量再减少 20%。
  • 针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%;
  • 当 Brotli 压缩级别为 1 时,压缩率比 Gzip 压缩等级为 9(最高)时还要高;
  • 在处理不同 HTML 文档时,Brotli 依然能够提供非常高的压缩率。
  • 兼容性:目前除了 IE 和 Opera Mini 之外,几乎所有的主流浏览器都已支持 Brotli 算法。

image.png

使用条件 >>>

  • Content-Length 大于 256 字节;
  • 客户端请求头需携带 Accept-Encoding: br;
  • Content-Type 满足如下 MIME 列表:
text/plain
text/javascript
text/css
text/xml
text/x-component
application/javascript
application/x-javascript
application/xml
application/json
application/xhtml+xml
application/rss+xml
application/atom+xml
application/x-font-ttf
application/vnd.ms-fontobject
image/svg+xml
image/x-icon
font/opentype
text/html -- default

vite项目开启 brotli 压缩 >>>

  • 使用 vite-plugin-compression (opens new window)对平台进行 gzip 或者 brotli 压缩,nginx 对这两种压缩模式都支持,压缩后部署到 nginx 将极大提高网页加载速度。
  • 修改 .env.production 文件,设置 VITE_COMPRESSION 全局变量即可,如下:
# 不开启压缩,默认
VITE_COMPRESSION = "none"
//以下配置压缩时不删除原始文件的配置
开启 gzip 压缩,
VITE_COMPRESSION = "gzip"

开启 brotli 压缩
VITE_COMPRESSION = "brotli"

# 同时开启 gzip 与 brotli 压缩
VITE_COMPRESSION = "both"

//以下配置压缩时删除原始文件的配置
开启 gzip 压缩
VITE_COMPRESSION = "gzip-clear"

开启 brotli 压缩
VITE_COMPRESSION = "brotli-clear"

# 同时开启 gzip 与 brotli 压缩
VITE_COMPRESSION = "both-clear"

4. 缓存

缓存的原理就是更快读写的存储介质+减少IO+减少CPU计算=性能优化。而性能优化的第一定律就是:优先考虑使用缓存。
缓存的主要手段有:浏览器缓存、CDN、反向代理、本地缓存、分布式缓存、数据库缓存。

浏览器缓存 可以看我的另一篇文章: 浅谈 强制缓存/协商缓存 怎么用?文中介绍了浏览器缓存的使用方式以及查看网站当前配置的缓存策略的方法。

5. Ajax可缓存

Ajax在发送的数据成功后,为了提高页面的响应速度和用户体验,会把请求的URL和返回的响应结果保存在缓存内,当下一次调用Ajax发送相同的请求(URL和参数完全相同)时,它就会直接从缓存中拿数据。 在进行Ajax请求的时候,可以选择尽量使用get方法,这样可以使用客户端的缓存,提高请求速度。

6. 组件按需引入

使用第三方组件库时,要按需引入,例如import { Button } from 'vant';

7. 动态加载

使用import()动态引入的语法引入一些第三方库和组件,例如我们在测试环境下开启 vconsole 调试:

if (location.host !== '正式环境域名') {
  import('@/utils/vconsole')
}

8. 组件异步加载

组件异步加载有两种方式:

// 1. import懒加载
() => import('@/pages/xxx.vue')
// 2. 使用require
resolve => require(['@/pages/xxx.vue'], resolve),

9. 路由懒加载

路由懒加载是组件异步加载的运用:

//routes
{
  path: '/index',
  name: 'index',
  component: () => import('@view/xxx.vue'),
  //或者 component: require(['@/view/xxx.vue'], resolve), 
  meta: { title: '首页' }
}

10. CDN内容分发

CDN的全称是Content Delivery Network,即内容分发网络。静态文件,音频,视频,js资源,图片等都可以放CDN上。
CDN的原理类似于京东,其实就是将资源放在遍布世界各地的服务器上,访问资源的时候,请求会重定向到 离用户最近的服务节点上。

11. 域名发散(域名收敛)

就是将同一站点下的静态资源分布在不同域名下。例如:
主站域名www.a.com
访问图片的域名www.a-img.com
访问文件的域名www.a-link.com
详情可以查看我的另一篇文章:
(域名发散)为什么主页面和静态资源要置于不同的域名下?

12. DNS预解析

当你的网站第一次请求某个跨域域名时,需要先解析该域名(例如页面访问cdn资源,第一次访问需要先解析cdn)。可以在请求的Timing上看到有一个DNS Lookup阶段,而在这个请求之后的其他该域名的请求都没有这项时间支出。

  <link rel="dns-prefetch" href="xxx.com"> 

使用说明: >>>

  • http页面下所有的a标签的href都会自动去启用DNS Prefetch,也就是说,你网页的a标签href带的域名,是不需要在head里面加上link手动设置的。https页面需要使用meta标签强制开启:
  <meta http-equiv="x-dns-prefetch-control" content="on">
  • dns-prefetch适用于网页引用了大量其他域名的资源,例如淘宝。

13. web worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

合理实用web worker可以优化复杂计算任务。这里直接抛阮一峰的入门文章:传送门

写在最后 🍺

一些优化的详细描述不便与在本文展开,不然万字长文没几个人愿意看🤣,所以挂了很多我的其他文章,都是以前在csdn发的,以后有时间了慢慢搬到掘金来~

客观看到这了,点个赞呗😘😘😘

相关文章链接:
预加载5种不同的方法
浅谈 强制缓存/协商缓存 怎么用?
(域名发散)为什么主页面和静态资源要置于不同的域名下?