Web极致性能优化指南

1:前言

在Web前端领域,性能优化是至关重要的一环。本文将带你深入了解前端应用运行时的关键因素,并分享如何通过优化代码结构、提高运行效率以及优化资源加载和管理的最佳实践来实现这一目标。无论你是初学者还是资深开发者,这篇文章都将为你提供实用的技巧指导,帮助你构建出色的Web应用。我们将从多个维度分析运行时的优化策略,并通过专业知识向你展示如何将这些策略转化为实际操作。

温馨提示:码字不易,先赞后看,养成习惯!!!

2:懒加载

2.1:路由懒加载

在路由系统中使用异步语法,实现按需加载,只有使用到该路由模块才会进行渲染加载处理。例如,可以使用 import() 函数来动态导入:

js 复制代码
export const basicRoutes = [
  {
    path: '/',
    redirect: '/menu/home'
  },
  {
    name: 'LoginTest',
    path: '/loginTest',
    component: () => import('@/views/login/indexTest.vue'),
    meta: {
      title: 'LoginTest'
    }
  }
]

2.2:异步组件加载(嵌套组件二层组件)

使用 Vue 提供的异步组件语法,内层实现基于promise。例如,可以使用 defineAsyncComponent() 函数来动态导入组件:

js 复制代码
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})

// 你可以像使用其他一般组件一样使用 `AsyncComp`。
// 最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。
// 它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

甚至此时我们可以使用到一些高级配置项目去定制我们想要的组件加载效果

js 复制代码
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,
  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})
 
// 如果提供了一个加载组件,它将在内部组件加载时先行显示。
// 在加载组件显示之前有一个默认的 200ms 延迟------这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
// 如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。
// 你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

顺便看一下项目中该如何实践: 其中leftListCommonDialog这两个组件你可以像正常组件一样的使用他们,并获取异步加载能力。

2.3:图片懒加载

1:getBoundingClientRect 实现

说到图片懒加载我们只需要找到元素位置计算与窗口的距离进行判断,当元素出现在窗口上或者快要出现的时候执行相关操作并渲染,反之。下面这个实例可以很直接的看到当向下滚动的时候会开始加载img,由于我们img文件不存在所以导致加载报错,但是这不影响我们实现这个功能,实际项目中实现基本是这个思路。 如下:

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片懒加载示例</title>
    <style>
      /* 初始状态下图片高度为0,等待加载 */
      img {
        height: 0;
      }
    </style>
  </head>
  <body>
    <!-- 占位符 -->
    <div style="height: 1000px"></div>
    <!-- 需要懒加载的图片 -->
    <img data-src="placeholder.jpg" alt="Lazy-loaded Image" />
    <script>
      // 获取所有需要懒加载的图片
      let lazyImages = document.querySelectorAll('img[data-src]')
      function lazyLoad() {
        lazyImages.forEach(function (img) {
          // 获取图片的位置信息
          let rect = img.getBoundingClientRect()
          // 如果图片进入了视口范围内
          if (rect.top >= 0 && rect.top <= window.innerHeight) {
            // 加载图片
            img.setAttribute('src', img.getAttribute('data-src'))
            // 移除data-src属性,避免重复加载
            img.removeAttribute('data-src')
          }
        })
        // 移除已加载的图片,以减少下次检查的数量
        // lazyImages = document.querySelectorAll('img[data-src]');
      }
      // 页面加载时执行一次懒加载
      lazyLoad()
      // 滚动时触发懒加载
      window.addEventListener('scroll', lazyLoad)
      // 窗口大小改变时触发懒加载
      window.addEventListener('resize', lazyLoad)
    </script>
  </body>
</html>

// 在这个示例中,图片的实际地址通过data-src属性指定,而不是直接通过src属性。 
// 页面加载时,脚本会获取所有具有data-src属性的图片元素,然后通过getBoundingClientRect()方法检查它们是否在视口范围内。
// 如果在视口范围内,则将data-src属性的值赋给src属性,从而加载图片。当滚动或调整窗口大小时,懒加载函数将被触发,检查并加载可见区域内的图片。
// 最后在页面卸载的时候清一下缓存即可。

(2)看一下视图应该会更加直观

  • top:目标右上角距视窗上边沿距离
  • left:目标右上角距视窗左边沿距离
  • bottom:目标左下角角距视窗上边沿距离
  • right:目标左下角距视窗左边沿距离

(3)观察一下兼容性如何:

(4)取巧方案:使用decoding="async"与loading="lazy"

如果你觉得上面的这些太麻烦,有没有简单的方式一样能实现呢?那么好消息是你可以直接用以上属性去直接代替麻烦的懒加载代码,非常方便简单且高效。

对兼容性要求不高的项目直接用这个就行,这些已经在底层实现了懒加载的动作稳妥且安全。

点击查看:MDN对该属性的介绍

3:使用 Web Worker

3.1:介绍

Web Worker 是 H5 提供的功能,允许在浏览器中创建多线程的 JavaScript 程序(js不是当线程吗?),用于执行长时间运行的任务而不会阻塞页面交互。

  • 计算密集型任务:例如对大型数据集的排序、搜索或图像处理等。通过将这些任务委托给 Web Worker,可以避免阻塞主线程,从而保持页面的响应性。
  • 网络请求:当需要执行大量的网络请求并对它们进行处理时,可以使用 Web Worker 来将这些任务分配给单独的线程,以提高性能和并行处理能力。
  • 实时数据处理:对于需要实时处理数据并产生反馈的应用程序,如游戏、音视频处理等,Web Worker 可以用于在后台执行计算任务,从而不影响用户体验。
  • 长时间运行的脚本:某些任务可能需要长时间运行,例如在后台执行定期的数据备份、计算复杂的算法等,这些任务可以使用 Web Worker 在后台执行,而不会影响页面的交互性能。
js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Worker 示例</title>
  </head>
  <body>
    <!-- 按钮用于启动和停止 Web Worker -->
    <button id="startButton">开始 Web Worker</button>
    <button id="stopButton" disabled>停止 Web Worker</button>
    <p id="result"></p>
    <script>
      // 定义变量来存储 Web Worker 实例 let worker;
      // 页面加载完成后执行的函数
      document.addEventListener('DOMContentLoaded', function () {
        // 获取按钮元素
        let startButton = document.getElementById('startButton')
        let stopButton = document.getElementById('stopButton')
        // 点击开始按钮时启动 Web Worker
        startButton.addEventListener('click', function () {
          // 检查浏览器是否支持 Web Worker
          if (typeof Worker !== 'undefined') {
            // 检查是否已经存在 Web Worker 实例
            if (typeof worker == 'undefined') {
              // 创建新的 Web Worker 实例
              worker = new Worker('worker.js')
              // 监听来自 Web Worker 的消息
              worker.onmessage = function (event) {
                // 将消息显示在页面上
                document.getElementById('result').innerHTML = event.data
              }
              // 更新按钮状态
              startButton.disabled = true
              stopButton.disabled = false
            }
          } else {
            // 浏览器不支持 Web Worker
            document.getElementById('result').innerHTML = '抱歉,您的浏览器不支持 Web Worker。'
          }
        })
        // 点击停止按钮时停止 Web Worker
        stopButton.addEventListener('click', function () {
          // 终止 Web Worker 实例
          worker.terminate()
          // 清除 worker 变量
          worker = undefined
          // 更新按钮状态
          startButton.disabled = false
          stopButton.disabled = true
          // 显示消息
          document.getElementById('result').innerHTML = 'Web Worker 已停止。'
        })
      })
    </script>
  </body>
</html>
js 复制代码
// 这段代码会在 Web Worker 中执行
let i = 0
function timedCount() {
  i = i + 1
  postMessage(i)
  setTimeout(timedCount(), 500) // 每隔 500ms 发送一次消息
}
timedCount()

我们要额外关注的是 Web Worker 的一些限制,或者说需要我们开发者去衡量使用的必要性,很多时候不是用了就一定好。 比如:不能访问dom、导致额外性能问题(在高负载下可能会使系统卡顿)、交互复杂等。

4:资源预加载

4.1:介绍

(1)prefetch

  • prefetch 是一种告诉浏览器在空闲时可以预加载资源的指令。它会在浏览器后台异步地下载指定的资源,并存储在浏览器缓存中,以备将来使用。
  • prefetch 适合用于加载用户即将访问的页面所需的资源,以加快后续页面的加载速度。例如,可以在当前页面上添加 prefetch 链接标签,以预加载下一个页面所需的 CSS 文件、JavaScript 文件或其他资源。

(2)preload

  • preload 是一种在当前页面加载时立即加载指定资源的指令。它会在浏览器优先级较高的下载队列中下载资源,并尽快应用到当前页面中。
  • preload 适合用于加载当前页面所需的重要资源,例如首屏所需的关键 CSS 文件、JavaScript 文件、字体文件等。通过在页面的头部添加 preload 标签,可以确保这些关键资源在页面渲染前已经被下载并准备就绪。

(3)preconnect:

  • preconnect 是一种优化网页性能的技术,它告诉浏览器在后续请求中预先建立到指定域名的连接。这样可以减少建立连接的时间,从而加速后续资源的加载。
  • preconnect 由于浏览器限制,不能持续存在,所以必要的资源才会做预连接设置。

4.2:使用

实际使用中只有开发者指定的某些资源才需要做这些指定加载方式,如果没有指定在 vite 中会默认给你加上:

当我们想对指定的资源进行预加载的时候可以通过<link>标签进行设置可以这样做。比如:

js 复制代码
<link rel="preload" as="script" href="xxx.js" />
<link rel="prefetch" as="script" href="xxx.js" />
js 复制代码
// 或者这样
<template>
  <router-link to="/about" prefetch>About</router-link>
  <router-link to="/contact" preload>Contact</router-link>
</template>

在实际开发中你可以指定的某些资源进行预操作,当打包编译的过程中打包器这些资源会进行特殊处理(注意在 webpackvite 中设置会有所区别),以保证用户设置的正确性。

5:脚本非阻塞异步加载

5.1:介绍

异步无阻塞加载 JavaScript 脚本是一种优化网页性能的技术,它可以在不阻塞页面渲染的情况下加载脚本文件,并在加载完成后立即执行。这种方式可以提高页面的加载速度和用户体验。

在 HTML 中,我们可以通过 <script> 标签的 asyncdefer 属性来实现异步加载脚本:

  1. async 属性
  • async 属性表示脚本的异步加载,它告诉浏览器立即开始下载脚本,但不会阻塞页面的解析和渲染。当脚本下载完成后,会立即执行,不管其他脚本是否已经下载完成。这意味着脚本的执行顺序不受控制,可能会与其在页面中的顺序不一致。
  • 使用 async 属性加载的脚本适用于独立、互相之间无依赖关系的脚本,例如用于分析、广告或跟踪的脚本。
  1. defer 属性
  • defer 属性表示脚本的延迟加载,它告诉浏览器立即开始下载脚本,但会延迟执行直到页面解析完成后、DOMContentLoaded 事件触发之前。多个 defer 脚本会按照它们在页面中出现的顺序依次执行,保证了执行顺序。
  • 使用 defer 属性加载的脚本适用于页面初始化时需要执行的脚本,例如用于初始化页面内容或绑定事件处理程序的脚本。

我们总结一下:

  • 使用 async 属性加载的脚本是异步的,可能会在页面解析过程中执行,不保证执行顺序(测试过多次其实也能按照顺序执行,可能在更复杂的环境下会出现)。
  • 使用 defer 属性加载的脚本是延迟加载的,会在页面解析完成后按照顺序执行。

5.2:使用

(1)异步加载第三方脚本 : 如果你需要在页面中加载第三方脚本,并且这些脚本不依赖于页面的其他内容,你可以使用 async 属性来异步加载它们。

例如,在 Vue 组件中的 mounted 钩子函数中动态创建 <script> 标签并设置 async 属性来加载第三方脚本。如下:

js 复制代码
export default {
  mounted() {
    const script = document.createElement('script');
    script.src = 'https://xxx.js';
    script.async = true;
    document.body.appendChild(script);
  }
}

(2)延迟加载初始化脚本 : 如果你有一些初始化脚本需要在页面加载完成后执行,但又不想阻塞页面渲染,你可以使用 defer 属性来延迟加载这些脚本。如下:

js 复制代码
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
 
// 在根组件的 mounted 钩子函数中延迟加载初始化脚本
app.mount('#app');
 
const initScript = document.createElement('script');
initScript.src = 'xxx.js';
initScript.defer = true;
document.body.appendChild(initScript);

这样可以确保初始化脚本在页面加载完成后执行,而不会阻塞页面的渲染。在使用 asyncdefer 属性加载脚本时,需要注意脚本的加载和执行顺序以及对页面的影响。确保选择适当的加载方式来优化页面加载性能和用户体验。

6:压缩

6.1:压缩

对于 To C 网站大部分存在大量的图片,我粗略的统计了一下图片所耗费的流量已经超过了整个站点的 60% 以上的流量,所以对图片的优化将至关重要也是个老生常谈的问题。

6.1.1:图片格式选择

我们站点选择的是webp,是一种全新一代的图片格式。由谷歌2010年推出。在《web前端性能优化》这本书中也有提及。该格式图片拥有当前市面上绝大多数图片的优点集一身,实际使用下来在同等视觉体验下可以将图片所占的内存空间减小20%-50%,是一个非常优秀的图片格式,假设即使我们不对图片进行压缩也能在获得更小的图片输出,极大节约我们的带宽,提升加载速度。 看一下 caniuse 在当前使用 webp 图片应该没有后顾之忧了,放心大胆用(IE已死!)。

6.1.2:图片压缩

对于图片压缩市面上有很多打包器插件都可以做,具体配置也比较简单想做统一压缩的可以选择这个 rollup的配置插件:rollup-plugin-imagemin,这个插件的配置以及如何使用不是本文的重点,有兴趣的可以去试用一下。

如果想单独压缩某一些大文件的推荐使用这个 图片压缩。个人觉得非常好用,压缩效果好(100%质量的情况下基本可以做到和原图无异,还能很大程度压缩大小),支持批量导入,批量下载,把需要压缩的图片批量导入选择压缩参数即可完成压缩。

6.1.3:svg压缩

一些使用比较简单的,重复使用的小图标可以用 svg 格式,svg 的好处不用我说了具体可以 看这一篇,有详细说明。但使用这个也是要注意的,就是对于复杂图标还是不建议使用 svg,相对而言其大小会变得非常巨大,得不偿失。还有一点就是别用多了,适量最好(个人经验是50个以内),多了会影响你的首页展示速度!具体可以观察一下这个文件的大小。没有优化之前这个文件将近 500k,现在只有大概 68k。首屏文件在网络上传输的时间大幅缩减。原来(300-500)ms --> 现在(50-150)ms 如果有可能尽量控制在 14.4k 以内,能会进一步提升 FCP (first content painting) 对于 svg 压缩,推荐 svgo 用过都说好!

1:首先下载改安装pnpm -g install svgo

2:准备一个文件夹来承接压缩后的 svg 文件

3:配置 svgo.config.js

js 复制代码
module.exports = {
  plugins: [
    'removeDoctype',
    'removeXMLProcInst',
    'removeComments',
    'removeMetadata',
    'removeEditorsNSData',
    'cleanupAttrs',
    'inlineStyles',
    'minifyStyles',
    // 'cleanupIDs',
    'removeUselessDefs',
    'cleanupNumericValues',
    'convertColors',
    'removeUnknownsAndDefaults',
    'removeNonInheritableGroupAttrs',
    'removeUselessStrokeAndFill',
    // 'removeViewBox',
    'cleanupEnableBackground',
    'removeHiddenElems',
    'removeEmptyText',
    'convertShapeToPath',
    'convertEllipseToCircle',
    'moveElemsAttrsToGroup',
    'moveGroupAttrsToElems',
    'collapseGroups',
    'convertPathData',
    'convertTransform',
    'removeEmptyAttrs',
    'removeEmptyContainers',
    'mergePaths',
    'removeUnusedNS',
    'sortDefsChildren',
    'removeTitle',
    'removeDesc'
  ]
}

配置项可以参考一下,具体可以去 GitHub 看一下每一项含义再做定制化的压缩,这里不多介绍

4:在 package.json 文件里设置命令行做配置即可

"svgo": "svgo -f <你的源文件地址> -o <输出的压缩文件地址> --config svgo.config.js"

5:将 svgo 作为配置命令嵌入到你项目的整个构建流程中即可

"build": "pnpm svgo && vite build"

使用起来非常方便,不受架构限制,简单配置过就可以跑起来了

6.1.4:小图片处理

由于站点中还有大量的 1-10kb小图片,但是不适用于 svg 那么我们可以通过配置来将其转成 base64url,图片被转换成类似于这样的一串字符串data:image/webp;base64,UklGRhALAABX...O/AA/fPxcP278tD9s/RA/Lv1pPoP9kAgA= 浏览器就不用再去下载,可以极大的减少 http 请求。但也有一些问题,转成 Base64 之后文件大约会增大 1/3,本质上网络还是要承担这一部分流量,具体是由于 Base64 要求把每三个8Bit的字节转换为四个 6Bit 的字节 (3*8 = 4*6 = 24),然后把 6Bit 再添两位高位0,组成四个 8Bit 的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。 可以通过以下的配置将小图片转 base64 项目中通过配置,建议配置10k以下的值,不配置默认4kb 直达链接

1:转换规则

关于这个编码的规则:

①把3个字节变成4个字节

②每76个字符加一个换行符

③最后的结束符也要处理

RFC 4648 标准的 Base64 索引表

2:例子

  • 首先,将二进制数据中每三组 8 个二进制位"重新分组为四组 6 个二进制位
  • 然后,每组的 6 个二进制位用一个十进制数来表示。6 个二进制位可表示的十进制数的范围是 0 - 63
  • 接下来,根据 Base64 索引表,将每组的十进制数转换成对应的字符,即每组可以用一个可打印字符来表示

ManBase64 编码结果为 TWFu,详细原理如下:

  • 优点:节约 http 请求
  • 缺点:项目文件稍许变大

6.1.5:文件gzip

在实际项目中我们还可以额外对代码进行进一步压缩使用到的插件是:vite-plugin-compression 该插件利用了现代浏览器对 Gzip 和 Brotli 压缩算法的支持,可以同时生成经过 Gzip 和 Brotli 压缩的版本,以确保在不同浏览器环境下都能获得最佳的压缩效果。通过在 Vite 项目中配置 vite-plugin-compression 插件,可以轻松地为你的静态资源添加压缩版本,从而提高页面性能和用户体验。 以下是一个我们项目中的实际使用示例压缩的文件包括css、html、js、svg、json等关键文件。

示例如下:

1:安装:pnpm i -g vite-plugin-compression

2:引入:import viteCompression from 'vite-plugin-compression'

3:使用:

js 复制代码
viteCompression({
  threshold: 1024,
  filter: /\.(css|html|js|svg|json)$/i,
  deleteOriginFile: false,
  algorithm: 'gzip'
})

配置好了后端支持一下就生效了,对比了一下整体 压缩了60% 左右,看一下效果

6.1.6:其他

terser 具体根据自己需求来定,配置项请 移步这里

7:文件hash(缓存)

js 复制代码
// vite.config.js
chunkFileNames: 'static/js/[name]-[hash].js', // 引入文件名的名称
entryFileNames: 'static/js/[name]-[hash].js', // 包的入口文件名称
assetFileNames: 'static/[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等

看一下效果

8:聚合碎片

8.1:介绍

聚合碎片的本意是要减少小文件的 http 请求,把这些小文件聚合到该页面需要请求的文件之中。想要的结果是一个http 请求可以涵盖几十个碎片请求(由于浏览器限制一个域下最多允许 6个tcp 同时存在),这样对于整个项目来说其实是非常有利的,把小文件直接并入大文件中,只需要拉取少数的几个文件就可以。减少了 tcp 连接次数,避免的大量的慢启动。同时也可以尽量的减少等待时间。所以综上站点策略将去聚合大量碎片,并尽量保持大文件个数控制在 6 个。

8.2:使用

通过 rollup 提供的 api,我们能抓住每一个碎片,将其按照每个页面进行高度的定制化。可以通过 id 这个参数进行正则匹配,进行分包或者聚合都行。思路和方式相似,按照自己的想法去写即可。

9:环境区分

环境区分这个思路比较纯粹,就是针对不同的环境做不同的配置策略。

比如:开发环境我希望能输出 log,那么就会在打包编译的时候去判断到底现在打包是哪一个环境,如果是开发,测试那么就会保留一些 log、debugger、err、sourcemap、comment 等,如果是生产环境就会屏蔽这些。

配置如下代码:

js 复制代码
// vite.config.js
sourcemap: !isPro,
minify: 'terser',
terserOptions: {
    compress: {
      drop_console: isPro, // 删除console
      drop_debugger: isPro // 删除 debugger
    },
    format: {
      comments: false // 去掉注释内容
    }
}

配置过你会发现,每个环境的包大小差距很大,生产环境的包可能只有开发环境的一半大小。

10: 动画

10.1:介绍

如果在项目中有存在定时动画需要使用到js控制动画,那么 requestAnimationFrame 将会是一个非常好的选择。requestAnimationFrame会在浏览器重绘之前执行回调函数的 JavaScript 方法。 通常用于执行动画或其他需要在屏幕刷新之前进行更新的任务,以确保动画的流畅性和性能。 当调用 requestAnimationFrame(callback) 时,浏览器会在下一次重绘之前执行指定的回调函数。 这意味着,当调用 requestAnimationFrame 时,浏览器会在适当的时间点调用回调函数,以便在下一次屏幕刷新时更新动画或执行其他操作。

具体来说,requestAnimationFrame 的运行实际上可以分为以下几个步骤:

  • 调用 requestAnimationFrame(callback):在 JavaScript 代码中调用 requestAnimationFrame 方法,并传入一个回调函数 callback。
  • 浏览器准备下一次重绘:浏览器会在适当的时间点准备进行下一次屏幕重绘。
  • 执行回调函数:在准备好下一次重绘时,浏览器会调用传入的回调函数 callback。这个回调函数通常用于更新动画状态或执行其他需要在屏幕刷新之前完成的任务。
  • 屏幕重绘:在执行完回调函数后,浏览器会进行屏幕重绘操作,将更新后的内容显示在屏幕上。
  • 循环执行:这个过程会一直循环执行,即每次调用 requestAnimationFrame(callback) 都会在下一次屏幕刷新时执行一次回调函数。

由于 requestAnimationFrame 的回调函数是在浏览器重绘之前执行的,因此它非常适合用于执行动画或其他需要在屏幕刷新之前进行更新的任务,以确保动画的平滑性和性能。

10.2:使用

以下将给出运行实例,有兴趣同学可以查看运行以下代码你会很清晰的看到setTimeout定时器动画与requestAnimationFrame动画之间的区别。

如下:

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>RequestAnimationFrame</title>
    <style>
      .box {
        width: 200px;
        height: 100px;
        position: absolute;
        top: 0;
      }
      #requestAnimationFrameBox {
        background-color: red;
        left: 10px;
      }
      #setTimeoutBox {
        background-color: green;
        left: 250px;
      }
    </style>
  </head>
  <body>
    <div class="box" id="requestAnimationFrameBox">requestAnimationFrame</div>
    <div class="box" id="setTimeoutBox">setTimeout</div>
    <script>
      // 使用 requestAnimationFrame 实现
      let requestAnimationFrameBox = document.getElementById('requestAnimationFrameBox')
      let requestAnimationFramePosition = 0
      let requestAnimationFrameSpeed = 3
      function animateWithRequestAnimationFrame() {
        requestAnimationFramePosition += requestAnimationFrameSpeed
        requestAnimationFrameBox.style.top = requestAnimationFramePosition + 'px'
        if (requestAnimationFramePosition > 600) {
          requestAnimationFramePosition = 0
        }
        requestAnimationFrame(animateWithRequestAnimationFrame)
      }
      animateWithRequestAnimationFrame()
      // 使用定时器实现
      let setTimeoutBox = document.getElementById('setTimeoutBox')
      let setTimeoutPosition = 0
      let setTimeoutSpeed = 3
      function animateWithSetTimeout() {
        setTimeoutPosition += setTimeoutSpeed
        setTimeoutBox.style.top = setTimeoutPosition + 'px'
        if (setTimeoutPosition > 600) {
          setTimeoutPosition = 0
        }
        setTimeout(animateWithSetTimeout, 1000 / 60) // 模拟每帧 60 次
      }
      animateWithSetTimeout()
    </script>
  </body>
</html>

以上是一个低负载环境下的对比展示,如果在高负载的环境中区别将更为明显,由于录屏并不能展示清晰实际情况,所以你可以自己直接运行以上代码查看区别。

更多细节请查阅MDN

11:代码流程控制

11.1:逻辑判断

(1)if-else还是switch?

对于条件判断语句我们认为哪一种使用会更好,需以实际情况去做选择。

接下来我将推荐一些使用的大体思路:

  1. 条件数量
  • 如果条件比较少,通常使用 if-else 是更简洁的选择。对于少量条件,使用 if-else 可以更直观地表达每个条件和相应的处理逻辑。
  • 如果条件比较多,并且每个条件之间是相互排他的,那么使用 switch 可能更清晰,因为它可以将多个条件组织在一起,更容易理解和维护。
  1. 条件类型
  • if-else 适用于对条件进行更复杂的判断,包括比较大小、比较字符串、逻辑运算等。它可以处理各种类型的条件判断,灵活性更高。
  • switch 通常用于对单个变量进行多个值的比较。它的条件只能是简单的相等判断,不支持比较大小、逻辑运算等。

例如:

js 复制代码
// 使用 if-else 结构实现
function getMonthNameWithIfElse(month) {
  let monthName;
  if (month === 1) {
      monthName = "January";
  } else if (month === 2) {
      monthName = "February";
  } else if (month === 3) {
      monthName = "March";
  } else if (month === 4) {
      monthName = "April";
  } else if (month === 5) {
      monthName = "May";
  } else if (month === 6) {
      monthName = "June";
  } else if (month === 7) {
      monthName = "July";
  } else if (month === 8) {
      monthName = "August";
  } else if (month === 9) {
      monthName = "September";
  } else if (month === 10) {
      monthName = "October";
  } else if (month === 11) {
      monthName = "November";
  } else if (month === 12) {
      monthName = "December";
  } else {
      monthName = "Invalid month";
  }
  return monthName;
}

// 使用 switch 结构实现
function getMonthNameWithSwitch(month) {
  let monthName;
  switch (month) {
      case 1:
          monthName = "January";
          break;
      case 2:
          monthName = "February";
          break;
      case 3:
          monthName = "March";
          break;
      case 4:
          monthName = "April";
          break;
      case 5:
          monthName = "May";
          break;
      case 6:
          monthName = "June";
          break;
      case 7:
          monthName = "July";
          break;
      case 8:
          monthName = "August";
          break;
      case 9:
          monthName = "September";
          break;
      case 10:
          monthName = "October";
          break;
      case 11:
          monthName = "November";
          break;
      case 12:
          monthName = "December";
          break;
      default:
          monthName = "Invalid month";
  }
  return monthName;
}
// 测试函数
function test() {
  const input = 6; // 测试月份为6,代表六月
  console.log("Using if-else:", getMonthNameWithIfElse(input));
  console.log("Using switch:", getMonthNameWithSwitch(input));
}
test();

// 以上两种逻辑的方案你觉得哪种更喜欢?

(2)索引优化

对于逻辑判断是不是只能采用逻辑判断语句进行?答案是否定的,我们也可以索引将逻辑判断进行变构处理。 如下:

js 复制代码
// 使用对象字面量优化条件判断逻辑
function getMonthName(month) {
  const months = {
      1: "January",
      2: "February",
      3: "March",
      4: "April",
      5: "May",
      6: "June",
      7: "July",
      8: "August",
      9: "September",
      10: "October",
      11: "November",
      12: "December"
  };
  return months[month] || "Invalid month";
}
// 测试函数
function test() {
  const input = 6; // 测试月份为6,代表六月
  console.log("Month name:", getMonthName(input));
}
test();

// 以上逻辑控制就很好的避免了条件判断,更加直观简洁

(3)逻辑释放

是否有思考过,假设我们的的键值的匹配是动态的,是不是(2)中的方案就不能采用了? 当然不是,既然是动态的那我们就应该赋予动态属性以方法去实现,而不是重新回到if else

改造如下:

js 复制代码
// 定义规则数组
const rules = [
  {
    match: function (month) {
      return month === 'January'
    },
    action: function () {
      return 'January'
    }
  },
  {
    match: function (month) {
      return month === 'February'
    },
    action: function () {
      return 'February'
    }
  },
  {
    match: function (month) {
      return month === 'March'
    },
    action: function () {
      return 'March'
    }
  }
]
// 定义函数
function _do(month, param) {
  for (let i = 0; i < rules.length; i++) {
    if (rules[i].match(month, param)) {
      return rules[i].action(month, param)
    }
  }
  return 'Invalid month'
}
// 测试函数
function test() {
  const input = 'January' // 测试月份为1,代表一月
  console.log('Month name:', _do(input))
}
test()

改造后你会发现代码量似乎变多了,但这个方案的通用性能力将非常好,对于多层嵌套的复杂逻辑将无害降解。 更重要的是后期的维护过程中你可以肆意的加入逻辑判断而完全不必担心会影响其他逻辑实现真正的释放解绑逻辑,同时实现对复杂的嵌套会进行最小化function解构。

12:虚拟列表

12.1:介绍

虚拟列表是一种用于优化大型列表或表格性能的技术,它只渲染可见区域的内容,而不是一次性渲染全部数据。 这种技术能够减少 DOM 元素的数量,提高页面加载速度和渲染性能。同时在高负载环境下相对的流畅度表现也非常好。

12.2:使用

以下是一个基础的代码实例,你也可以将这段代码放入你的项目中稍加改造就可以当做一个通用性虚拟列表组件。

如下:

js 复制代码
<template>
  <!-- 虚拟列表容器 -->
  <div class="virtual-list" ref="listContainer" @scroll="handleScroll">
    <!-- 占位元素,用于撑开列表高度 -->
    <div class="list-placeholder" :style="{ height: totalHeight + 'px' }"></div>
    <!-- 可见列表项 -->
    <div
      class="list-item"
      v-for="(item, index) in visibleItems"
      :key="index"
      :style="{ height: itemHeight + 'px', transform: 'translateY(' + item.pos + 'px)' }"
    >
      {{ item.name }}
    </div>
  </div>
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
 
// 数据
const listData = ref([]) // 列表数据
const visibleItems = ref([]) // 可见的列表项
const itemHeight = 50 // 列表项高度
let startIndex = 0 // 开始索引
let visibleItemCount = 0 // 可见列表项数量
let containerHeight = 0 // 容器高度
let totalHeight = 0 // 总高度
// Refs
const listContainer = ref(null) // 列表容器的引用
// 计算可见列表项
const calculateVisibleItems = () => {
  visibleItemCount = Math.ceil(containerHeight / itemHeight) // 计算可见列表项数量
  visibleItems.value = listData.value.slice(startIndex, startIndex + visibleItemCount) // 更新可见列表项
}
// 更新列表数据
const updateList = () => {
  totalHeight = listData.value.length * itemHeight // 计算总高度
}
// 滚动事件处理函数
const handleScroll = () => {
  const scrollTop = listContainer.value.scrollTop // 获取滚动条位置
  const maxScrollTop = totalHeight - containerHeight - 50 // 计算最大滚动高度(减去一个缓冲值)
  // 根据滚动位置确定开始索引
  if (scrollTop === 0) {
    startIndex = 0
  } else if (scrollTop >= maxScrollTop) {
    startIndex = Math.max(listData.value.length - visibleItemCount, 0) // 在底部时,调整开始索引确保最后一项可见
  } else {
    startIndex = Math.floor(scrollTop / itemHeight)
  }
  calculateVisibleItems() // 更新可见列表项
}
// 模拟获取数据
const fetchData = () => {
  for (let i = 0; i < 101; i++) {
    listData.value.push({ name: `Ak_${i}`, pos: i * itemHeight }) // 添加数据
  }
  updateList() // 更新列表
}
// 组件挂载后执行
onMounted(() => {
  setTimeout(() => {
    containerHeight = listContainer.value.clientHeight // 获取容器高度
    calculateVisibleItems() // 计算可见列表项
  }, 100) // 延迟执行,等待DOM渲染完成
})
fetchData() // 获取数据
</script>
 
<style>
.virtual-list {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  position: relative;
}
.list-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
.list-item {
  border-bottom: 1px solid #ccc;
  line-height: 50px;
  padding: 0 10px;
}
</style>

// 对于虚拟列表而言还有很多细节需要探讨,比如当数据位动态高度的时候应该如何处理。
// 所以要做好一个更加通用的方案需要更多的付出与思考,代码要写好不容易啊。

12.3:运行

13:减少重绘重排

13.1:使用 CSS3 动画和过渡

CSS3 提供了硬件加速的动画和过渡效果,可以减少重排和重绘的次数。尽量避免使用 JavaScript 进行动画,因为它会导致大量的重排和重绘。

js 复制代码
<template>
  <div>
    <button @click="toggleBox">Toggle Box</button>
    <transition name="fade">
      <div v-if="showBox" class="box"></div>
    </transition>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      showBox: false
    }
  },
  methods: {
    toggleBox() {
      this.showBox = !this.showBox
    }
  }
}
</script>
 
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.box {
  width: 100px;
  height: 100px;
  background-color: red;
}
</style>

13.2:使用 CSS3 transform 属性

对于需要频繁操作的元素,如位移、缩放和旋转等,可以使用 CSS3 的 transform 属性来实现,因为 transform 不会触发重排和重绘。

js 复制代码
<template>
  <div>
    <button @click="toggleBox">Toggle Box</button>
    <div :class="{ box: showBox, hidden: !showBox }"></div>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      showBox: false
    }
  },
  methods: {
    toggleBox() {
      this.showBox = !this.showBox
    }
  }
}
</script>
 
<style>
.box {
  width: 100px;
  height: 100px;
  background-color: red;
  transition: transform 0.5s, opacity 0.5s;
}
 
.hidden {
  opacity: 0;
  transform: scale(0);
}
</style>

13.3:使用 will-change 属性

可以使用 will-change 属性来提示浏览器该元素将要发生改变,从而使浏览器提前进行优化,减少重排和重绘的次数。

js 复制代码
<template>
  <div>
    <button @click="moveBox">Move Box</button>
    <div class="box" :class="{ active: isActive }"></div>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      isActive: false
    }
  },
  methods: {
    moveBox() {
      this.isActive = !this.isActive
    }
  }
}
</script>
 
<style>
.box {
  width: 100px;
  height: 100px;
  background-color: red;
  transition: transform 0.5s;
}
 
.box.active {
  transform: translateX(200px);
  will-change: transform;
}
</style>

13.4:合并和最小化样式表和脚本

减少 HTTP 请求次数可以减少资源加载时间,从而减少重绘和重排的次数。可以使用工具将多个样式表和脚本文件合并成一个,或者使用 CSS 和 JavaScript 的压缩工具来减小文件大小。 在打包那一章节有具体实现:参阅

13.5:使用 requestAnimationFrame

使用 requestAnimationFrame 来执行动画和更新页面内容,这样可以确保动画的帧率稳定,减少因为频繁的重绘和重排而导致的性能问题。不多赘述,前文有实现案例。

13.6:避免频繁操作 DOM

避免频繁地操作 DOM,尽量一次性进行多个 DOM 修改,或者将多个 DOM 操作合并成一个操作,减少重排和重绘的次数。

js 复制代码
// 创建一个 DocumentFragment 对象
const fragment = document.createDocumentFragment();
// 模拟需要频繁操作的 DOM 元素列表
const data = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
// 遍历数据并创建 DOM 元素
data.forEach(item => {
    const listItem = document.createElement('li');
    listItem.textContent = item;
    fragment.appendChild(listItem); // 将创建的 DOM 元素添加到 DocumentFragment 中
});
// 找到需要插入的容器元素
const container = document.getElementById('container');
// 一次性将 DocumentFragment 中的所有 DOM 元素添加到容器中
container.appendChild(fragment);
 
// 思路就是创建文档碎片,用变量存操作,最后操作完毕统一交给浏览器渲染

13.7:优化图片和媒体资源

对于图片和媒体资源,可以使用适当的压缩算法来减小文件大小,从而减少资源加载时间和页面重排的次数。 这个也不多赘述压缩那一章节有具体实践:参考

相关推荐
小小小小宇7 分钟前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖42 分钟前
http的缓存问题
前端·javascript·http
小小小小宇1 小时前
请求竞态问题统一封装
前端
loriloy1 小时前
前端资源帖
前端
源码超级联盟1 小时前
display的block和inline-block有什么区别
前端
GISer_Jing1 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂1 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端2 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端
葡萄糖o_o2 小时前
ResizeObserver的错误
前端·javascript·html
AntBlack2 小时前
Python : AI 太牛了 ,撸了两个 Markdown 阅读器 ,谈谈使用感受
前端·人工智能·后端