Web极致性能优化指南

1:前言

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

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

2:懒加载

2.1:路由懒加载

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

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() 函数来动态导入组件:

import { defineAsyncComponent } from 'vue'

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

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

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

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

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

2.3:图片懒加载

1:getBoundingClientRect 实现

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

<!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:目标左下角距视窗左边沿距离

Web极致性能优化指南

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

Web极致性能优化指南

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

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

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

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

3:使用 Web Worker

3.1:介绍

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

  • 计算密集型任务:例如对大型数据集的排序、搜索或图像处理等。通过将这些任务委托给 Web Worker,可以避免阻塞主线程,从而保持页面的响应性。
  • 网络请求:当需要执行大量的网络请求并对它们进行处理时,可以使用 Web Worker 来将这些任务分配给单独的线程,以提高性能和并行处理能力。
  • 实时数据处理:对于需要实时处理数据并产生反馈的应用程序,如游戏、音视频处理等,Web Worker 可以用于在后台执行计算任务,从而不影响用户体验。
  • 长时间运行的脚本:某些任务可能需要长时间运行,例如在后台执行定期的数据备份、计算复杂的算法等,这些任务可以使用 Web Worker 在后台执行,而不会影响页面的交互性能。
<!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>
// 这段代码会在 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 中会默认给你加上:

Web极致性能优化指南

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

<link rel="preload" as="script" href="xxx.js" />
<link rel="prefetch" as="script" href="xxx.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> 标签的 async 和 defer 属性来实现异步加载脚本:

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

我们总结一下:

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

5.2:使用

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

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

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

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

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);

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

6:压缩

6.1:压缩

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

6.1.1:图片格式选择

我们站点选择的是webp,是一种全新一代的图片格式。由谷歌2010年推出。在《web前端性能优化》这本书中也有提及。该格式图片拥有当前市面上绝大多数图片的优点集一身,实际使用下来在同等视觉体验下可以将图片所占的内存空间减小20%-50%,是一个非常优秀的图片格式,假设即使我们不对图片进行压缩也能在获得更小的图片输出,极大节约我们的带宽,提升加载速度。
Web极致性能优化指南
看一下 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
Web极致性能优化指南
如果有可能尽量控制在 14.4k 以内,能会进一步提升 FCP (first content painting)
对于 svg 压缩,推荐 svgo 用过都说好!

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

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

Web极致性能优化指南

3:配置 svgo.config.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"

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

Web极致性能优化指南

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
Web极致性能优化指南
项目中通过配置,建议配置10k以下的值,不配置默认4kb
直达链接

1:转换规则

关于这个编码的规则:

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

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

③最后的结束符也要处理

RFC 4648 标准的 Base64 索引表
Web极致性能优化指南

2:例子

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

ManBase64 编码结果为 TWFu,详细原理如下:
Web极致性能优化指南

  • 优点:节约 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:使用:

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

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

Web极致性能优化指南

6.1.6:其他

Web极致性能优化指南
terser 具体根据自己需求来定,配置项请 移步这里

7:文件hash(缓存)

Web极致性能优化指南

Web极致性能优化指南

Web极致性能优化指南

Web极致性能优化指南

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

看一下效果

Web极致性能优化指南

8:聚合碎片

8.1:介绍

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

Web极致性能优化指南

8.2:使用

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

9:环境区分

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

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

配置如下代码:

// 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动画之间的区别。

如下:

<!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 通常用于对单个变量进行多个值的比较。它的条件只能是简单的相等判断,不支持比较大小、逻辑运算等。

例如:

// 使用 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)索引优化

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

// 使用对象字面量优化条件判断逻辑
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

改造如下:

// 定义规则数组
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:使用

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

如下:

<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:运行

Web极致性能优化指南

13:减少重绘重排

13.1:使用 CSS3 动画和过渡

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

<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 不会触发重排和重绘。

<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 属性来提示浏览器该元素将要发生改变,从而使浏览器提前进行优化,减少重排和重绘的次数。

<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 操作合并成一个操作,减少重排和重绘的次数。

// 创建一个 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:优化图片和媒体资源

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

14:计算样式

14.1:介绍

计算样式是指浏览器根据 CSS 样式规则和元素的属性值计算出最终应用到元素上的样式。
在前端开发中,有时候我们需要获取元素的最终样式信息,这就需要使用计算样式(Computed Style)。
在 JavaScript 中,可以通过 window.getComputedStyle(element) 方法来获取元素的计算样式。
这个方法返回一个 CSSStyleDeclaration 对象,包含了所有应用到指定元素的样式信息。

14.2:实例

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Style Calculation Optimization</title>
<style>
.box {
width: 100px;
height: 100px;
background-color: blue;
}
</style>
</head>
<body>
<div class="box" id="box"></div>
<button onclick="changeColor()">Change Color</button>
<script>
// 使用缓存存储计算好的样式值
const styleCache = {}
// 获取元素的背景颜色
function getBackgroundColor(element) {
debugger
// 检查缓存中是否已经计算过
if (styleCache[element.id] && styleCache[element.id].backgroundColor) {
return styleCache[element.id].backgroundColor
}
// 计算样式并存储到缓存中
const computedStyle = window.getComputedStyle(element)
styleCache[element.id] = {
backgroundColor: computedStyle.backgroundColor
}
return computedStyle.backgroundColor
}
// 改变元素的背景颜色
function changeColor() {
const box = document.getElementById('box')
const currentColor = getBackgroundColor(box)
debugger
// 模拟改变背景颜色
box.style.backgroundColor = currentColor === 'rgb(0, 0, 255)' ? 'red' : 'blue'
}
</script>
</body>
</html>
// 使用缓存,避免重复计算。在需要获取元素的计算样式时,建议使用window.getComputedStyle()方法,而不是直接访问元素的style属性。
// 因为getComputedStyle()返回的是一个只读的CSSStyleDeclaration对象,可以获取到元素的所有计算样式,包括外部样式表和内联样式。

15:内存管理

15.1:介绍:

内存泄漏的本质是什么?准确的说是:无法被垃圾回收器正确回收的内存。

15.2:使用

什么是无法回收的内存?经典的理论是无法触达。简单而言就是开发者自己都访问不到的内存当然可以认为是无法触达。程序可以放心大胆的回收。但是程序往往是非常小心的处理这些问题,很多时候回收算法也不能确定这块内存到底能不能被回收,既然不能确定,那就放着吧。久而久之泄露就出现了。那么我们要如何应对?其实也很简单,就是明确告诉程序运行之后我将不再需要这段内存。

接下来我们介绍一些通用型解决方案:

(1)移除定时器与内部关联项

<template>
<div>
<button @click="toggleCreation">Toggle DOM Creation</button>
<div ref="container"></div>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue';
const container = ref(null);
let intervalId = null;
let domCount = 0;
const toggleCreation = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
} else {
// 每秒创建一个新的 DOM 元素
intervalId = setInterval(() => {
const newDiv = document.createElement('div');
newDiv.textContent = `DOM Element ${domCount++}`;
container.value.appendChild(newDiv);
}, 1000);
}
};
onBeforeUnmount(() => {
// 在组件销毁前清理定时器
clearInterval(intervalId);
container.value = null
});
</script>

(2)移除监听

Web极致性能优化指南

(3)减少dom渲染

很好理解,将本来要大量渲染的结点通过某种方式只渲染用户看得见的区域。
上文中的 虚拟列表 即是,这一点非常重要,当出现大节点将会非常麻烦。

(4)打印移除

这个可以通过项目统一配置不管你是webpack还是rollup都有对应的配置项,一劳永逸。如下:

Web极致性能优化指南

(5)意外的环形引用

// 循环引用导致内存泄漏的示例
let obj1 = {};
let obj2 = {};
obj1.ref = obj2; // obj1 引用了 obj2
obj2.ref = obj1; // obj2 引用了 obj1
// 此时 obj1 和 obj2 形成了循环引用,无法被垃圾回收
// 即使不再需要它们,它们也不会被释放,导致内存泄漏

(6)被忽略的闭包

闭包存在很多争议,到底是不是闭包的锅,我认为是闭包没啥问题,是我们日常使用的过程中不注意导致的(有的确实非常隐蔽)。

我们以例子展开细说,如下:

function createClosure() {
let bigData = new Array(1000000).fill('data'); // 大量数据,占用较多内存
// 返回一个闭包函数
return function() {
// 闭包中引用了外部作用域的变量 bigData
console.log(bigData.length);
};
}
// 调用 createClosure,返回闭包函数
let closure = createClosure();
// 是不是很熟悉,createClosure 函数返回了一个闭包函数,闭包函数中引用了外部作用域中的 bigData 变量。
// 即使在外部作用域中不再需要 bigData 变量,闭包仍然持有对它的引用,导致 bigData 无法被释放,从而造成内存泄漏。
// 为了避免闭包导致的内存泄漏,应该及时释放不再需要的外部作用域变量的引用,或者将需要长期持有的变量放在全局作用域中。
// 在程序运行结束后手动将 closure = null,对于垃圾回收算法就能很快进行回收动作避免泄露。

再看一个比较隐蔽的例子,如下:

function createClosure() {
let bigData = new Array(1000000).fill('data') // 大量数据,占用较多内存
function _int() {
console.log(bigData)
}
// 返回一个普通函数
return function () {
console.log(1)
}
}
// 调用 createClosure
let closure = createClosure()
// 在这个例子中 closure 与 bigData 看似已经毫无关系,认知上这好像也毫无问题。但实际上,内存还是被保留了下来,往往这种代码容易让我们掉以轻心。
// 在我们的代码中经常出现这种结构,稍不注意就会掉坑里。

16:缓存

16.1:http缓存

1:缓存设置:

HTTP 缓存是通过设置 HTTP 响应头来控制浏览器对资源的缓存,从而提高网站的性能和加载速度。下面详细说明如何利用 HTTP 缓存进行优化:

(1)Cache-Control:使用 Cache-Control 头来指定缓存策略,常见的指令包括:

  • max-age=<seconds>:指定资源在缓存中的有效期时间,单位为秒。
  • no-cache:表示缓存需要重新验证,但仍可使用缓存。
  • no-store:表示不使用任何缓存,每次请求都要向服务器请求完整的资源。
  • public:表示响应可以被任何缓存存储。
  • private:表示响应只能在特定条件下被缓存。
  • immutable:表示资源不会发生变化,可以永久缓存。

例如,设置资源缓存有效期为一小时:

Cache-Control: max-age=3600

(2)Expires

  • 使用 Expires 头来指定资源的过期时间,是一个绝对时间,需要使用 GMT 格式的日期字符串。
  • 这个头和 Cache-Control 中的 max-age 是互斥的,推荐使用 Cache-Control 头 (在h1.1,Expires被Cache-Control替代)

例如,设置资源过期时间为 2025 年 12 月 31 日:

Expires: Sat, 31 Dec 2025 23:59:59 GMT

(3)Last-Modified 头和 If-Modified-Since

  • 使用 Last-Modified 头来指定资源的最后修改时间。
  • 客户端在发送请求时,可以使用 If-Modified-Since 头将上次的修改时间发送给服务器,服务器可以根据这个时间判断资源是否有更新,如果没有更新则返回 304 Not Modified 状态码,客户端直接从缓存中获取资源。

例如:

Last-Modified: Tue, 15 Feb 2022 12:00:00 GMT
If-Modified-Since: Tue, 15 Feb 2022 12:00:00 GMT

(4)ETag 头和 If-None-Match

  • 使用 ETag 头来指定资源的唯一标识符。
  • 客户端在发送请求时,可以使用 If-None-Match 头将上次请求返回的 ETag 值发送给服务器,服务器可以根据这个值判断资源是否有更新,如果没有更新则返回 304 Not Modified 状态码。

例如:

ETag"abc123"
If-None-Match"abc123"
// 注意:Etag 的校验优先级高于 Last-Modified。

2:缓存策略:

(1)强缓存

强缓存是指浏览器在请求资源时,不向服务器发送请求,而是直接从本地缓存中获取资源。强缓存可以通过设置响应头中的 Cache-Control 和 Expires 字段来实现。常见的设置方式包括:

  • Cache-Control: max-age=3600:表示资源在本地缓存中可以被缓存 3600 秒(1 小时)。
  • Expires: Wed, 21 Oct 2026 07:28:00 GMT:表示资源在本地缓存中的过期时间。

(2)协商缓存

协商缓存是指浏览器在请求资源时,先向服务器发送一个请求。服务器根据资源的特征信息(如 ETag 或 Last-Modified 字段)来判断资源是否有更新,如果没有更新,返回 304 Not Modified 状态码,浏览器直接从本地缓存中获取。

(3)缓存控制

通过设置响应头中的 Cache-Control 字段来控制缓存的行为,包括:

  • public:响应可以被任何设备缓存。
  • private:响应只能被浏览器缓存。
  • no-cache:浏览器需要先向服务器发送请求验证缓存是否过期。
  • no-store:响应不应该被缓存。
  • max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒。
  • s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言

(4)资源指纹

为了避免浏览器在缓存中保存过期的资源,可以在资源 URL 中添加一个唯一标识符(通常是文件内容的哈希值),即资源指纹。每次文件内容发生变化时,URL 中的资源指纹也会随之变化,从而迫使浏览器重新下载资源。通常我们在构建工具中做相应的配置即可。

Web极致性能优化指南

(5)缓存分组

将网站的资源按照类型或用途进行分组,并为每个分组设置单独的缓存策略。例如,可以将静态资源(如 CSS、JavaScript、图片)和动态内容(如 HTML 页面、API 请求)分别设置不同的缓存策略,以提高缓存的命中率。

3:实例:

在实际开发中,可以通过服务器端和前端两个方面来设置 HTTP 缓存策略。

(1)服务器端设置:

  1. 通过服务器配置文件设置:

   对于 Apache 服务器,可以通过 .htaccess 文件来设置缓存策略,例如使用 ExpiresByType 指令设置不同类型的文件的过期时间。
对于 Nginx 服务器,可以在配置文件中使用 expires 指令来设置缓存策略。

  1. 通过后端代码设置:

   在后端代码中,可以通过设置响应头来控制缓存策略,例如在返回资源的响应中设置 Cache-Control、Expires、Last-Modified 和 ETag 头。

(2)前端设置:

  1. 通过构建工具设置:

在前端项目的构建过程中,可以使用构建工具(如 Webpack、Parcel 等)来设置 HTTP 缓存策略,例如在构建时为静态资源添加哈希值,并将哈希值作为文件名,从而实现缓存的版本控制。

如下:

// vite.config.js
import { defineConfig } from 'vite';
import { minifyHtml } from 'vite-plugin-html';
export default defineConfig({
plugins: [
minifyHtml(),
{
name: 'custom-http-cache',
transformIndexHtml(html) {
// 在这里可以对 index.html 进行自定义转换
// 例如,添加缓存相关的响应头
return {
html: html.replace(
/<head>/,
`<head><meta http-equiv="Cache-Control" content="max-age=3600, public">`
)
};
}
}
]
});
  1. 通过手动设置:

在编写前端代码时,可以通过手动设置 HTTP 头来控制缓存策略。
例如:使用 JavaScript 中的 fetch 或者 XMLHttpRequest 对象发送请求时,可以通过设置 Cache-Control、Expires、If-Modified-Since 和 If-None-Match 头来实现缓存控制。

如下:

Web极致性能优化指南

综上所述,在实际开发中可以通过服务器端和前端两个方面来设置 HTTP 缓存策略,从而实现对网站资源的缓存控制,提高网站的性能和用户体验。具体的设置方法可以根据项目的需求和技术栈来选择适合的方式。

16.2:本地缓存

通常我们本地缓存指的是:利用浏览器提供的api去存取一些我们需要数据。

如下:

(1)Cookie

作用:Cookie 是一种存储在客户端的小型文本文件,用于存储用户的身份认证信息、会话状态等。它通常用于在客户端和服务器之间传递状态信息。

特点:Cookie 可以设置过期时间,可以在同一域名下被不同页面共享,但每个 Cookie 的大小通常受到限制,且会随着每次 HTTP 请求发送到服务器。

使用方法:可以通过 JavaScript 使用 document.cookie API 来读取、设置和删除 Cookie。

(2)Web Storage

作用:Web Storage 提供了一种在浏览器端保存数据的简单方式,包括 localStorage 和 sessionStorage 两种类型。

特点

  • localStorage:永久性存储,除非被用户手动清除,否则数据会一直保留在浏览器中。
  • sessionStorage:会话期间存储,数据在会话结束后被清除,只在当前会话中有效。

使用方法:可以通过 JavaScript 使用 localStorage 和 sessionStorage 对象的 API 来读取、设置和删除数据。

(3)IndexedDB

作用:IndexedDB 是浏览器提供的客户端数据库,用于存储大量结构化数据,支持高级的查询和事务操作。

特点:IndexedDB 是一个异步的数据库系统,支持对象存储和索引,适用于需要大规模数据存储和离线访问的应用场景。

使用方法:可以通过 JavaScript 使用 IndexedDB API 来创建数据库、存储数据、查询数据等操作。

// 打开数据库
const request = indexedDB.open('myDatabase', 1);
// 数据库打开成功
request.onsuccess = function(event) {
const db = event.target.result;
// 添加数据
const transaction = db.transaction(['customers'], 'readwrite');
const objectStore = transaction.objectStore('customers');
objectStore.add({ id: 1, name: 'John', email: 'john@example.com' });
// 查询数据
const getRequest = objectStore.get(1);
getRequest.onsuccess = function(event) {
console.log('Customer:', event.target.result);
};
// 删除数据
const deleteRequest = objectStore.delete(1);
deleteRequest.onsuccess = function(event) {
console.log('Data deleted successfully.');
};
};
// 数据库打开失败
request.onerror = function(event) {
console.error('Database error:', event.target.error);
};
// 创建数据库和对象存储空间
request.onupgradeneeded = function(event) {
const db = event.target.result;
// 创建对象存储空间
const objectStore = db.createObjectStore('customers', { keyPath: 'id' });
// 添加索引
objectStore.createIndex('name', 'name', { unique: false });
objectStore.createIndex('email', 'email', { unique: true });
};

目前感觉使用门槛较高,推荐使用三方库降低使用成本:localForage

(4)Cache Storage

作用:Cache Storage 是浏览器提供的用于存储缓存数据的 API,通常用于存储 Service Worker 缓存的资源。

特点:Cache Storage 可以将请求过的资源缓存起来,以便在离线或者网络不稳定的情况下能够快速加载资源,提高应用的可靠性和性能。

使用方法:可以通过 JavaScript 使用 Cache Storage API 来操作缓存,包括添加缓存、删除缓存、查询缓存等操作。

(5)总结

通过合理地使用这些客户端存储技术,可以实现对数据的持久化存储、快速读取和高效管理,提高应用的性能和用户体验。不同的存储技术适用于不同的场景,开发者可以根据需求选择合适的存储方式。

17:性能指标监控

首先,当我们想了解web的性能时我们该怎么办?我们需要解决一下几个问题:

(1)获取数据

(2)计算结果

17.1:获取数据

我们先看一张图:

Web极致性能优化指南
简而言之我们需要拿到这些数据,我们可以通过window.performance.timing拿到相关数据,如下:

Web极致性能优化指南

这么多属性对应的都是什么意思呢?

  1. navigationStart:浏览器开始导航的时间戳,通常是在地址栏输入网址回车或者点击链接开始加载页面时的时间。
  2. unloadEventStart:前一个页面的卸载(unload)事件开始的时间戳,如果没有前一个页面,该值为0。
  3. unloadEventEnd:前一个页面的卸载(unload)事件完成的时间戳,如果没有前一个页面,该值为0。
  4. redirectStart:重定向开始的时间戳,如果没有重定向,该值为0。
  5. redirectEnd:重定向结束的时间戳,如果没有重定向,该值为0。
  6. fetchStart:浏览器开始获取页面资源的时间戳,包括从缓存读取或者从网络下载资源。
  7. domainLookupStart:域名查询开始的时间戳,如果使用了持久连接(persistent connection),该值等同于 fetchStart。
  8. domainLookupEnd:域名查询结束的时间戳,如果使用了持久连接(persistent connection),该值等同于 fetchStart。
  9. connectStart:HTTP(TCP)连接开始的时间戳,如果使用持久连接,该值等同于 fetchStart。
  10. connectEnd:HTTP(TCP)连接完成的时间戳,如果使用持久连接,该值等同于 fetchStart。
  11. secureConnectionStart:HTTPS 安全连接开始的时间戳,如果不是安全连接或者使用持久连接,该值为0。
  12. requestStart:向服务器发送请求的时间戳,如果使用持久连接,该值等同于 fetchStart。
  13. responseStart:从服务器接收到第一个字节的时间戳,如果没有从服务器接收到响应,该值为0。
  14. responseEnd:接收到响应的时间戳,如果没有从服务器接收到响应,该值为0。
  15. domLoading:开始解析 HTML 文档的时间戳,通常是开始解析标记(HTML)。
  16. domInteractive:HTML 文档解析完成并且 DOM 构建完成的时间戳,可以开始渲染页面,但是页面中的资源(如图片、样式表)可能还在加载中。
  17. domContentLoadedEventStart:DOMContentLoaded 事件开始的时间戳,此时页面的 DOM 已经完全构建,但是可能还有一些异步资源(如图片、样式表)正在加载。
  18. domContentLoadedEventEnd:DOMContentLoaded 事件完成的时间戳,此时页面的所有同步资源已经加载完成。
  19. domComplete:页面加载完成的时间戳,所有资源都已经加载完成,包括异步加载的资源。
  20. loadEventStart:load 事件开始的时间戳,此时页面的所有资源都已经加载完成,但是可能还有一些异步任务在执行。
  21. loadEventEnd:load 事件完成的时间戳,此时页面的所有资源加载和执行都已经完成,页面已经完全加载。

有了这些数据我们就可以进入第二步

17.2:计算结果

(1)计算代码:

// dns耗时
const dns = performance.timing.domainLookupEnd - performance.timing.domainLookupStart
// tcp连接耗时
const tcp = performance.timing.connectEnd - performance.timing.connectStart
// 重定向耗时
const redirection = performance.timing.redirectEnd - performance.timing.redirectStart;
// 首字节接收耗时
const ttfb = performance.timing.responseStart - performance.timing.navigationStart
// 请求耗时
const request = performance.timing.responseStart - performance.timing.requestStart
// 加载耗时
const load = performance.timing.loadEventEnd - performance.timing.loadEventStart
// 加载总耗时
const loaded = performance.timing.loadEventStart - performance.timing.navigationStart
// dom加载总耗时
const DOMContentLoaded = performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart

(2)项目实现:

// 插件
export default {
install(app) {
// 监听页面加载完成事件
window.addEventListener('load', function () {
// debugger
// dns耗时
const dns = performance.timing.domainLookupEnd - performance.timing.domainLookupStart
// tcp连接耗时
const tcp = performance.timing.connectEnd - performance.timing.connectStart
// 重定向耗时
const redirection = performance.timing.redirectEnd - performance.timing.redirectStart
// 首字节接收耗时
const ttfb = performance.timing.responseStart - performance.timing.navigationStart
// 请求耗时
const request = performance.timing.responseStart - performance.timing.requestStart
// 加载耗时
const load = performance.timing.loadEventEnd - performance.timing.loadEventStart
// 加载总耗时
const loaded = performance.timing.loadEventStart - performance.timing.navigationStart
// dom加载总耗时
const DOMContentLoaded =
performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart
// 聚合数据
const timingObj = {
dns,
tcp,
redirection,
ttfb,
request,
load,
loaded,
DOMContentLoaded
}
throwTiming(timingObj)
})
// 抛出数据
function throwTiming(timing) {
// 这里可以执行检查、分析、上报等动作
console.log('Timing data:', timing)
}
}
}
// main.js
import performanceMonitor from '../build/plugin/performanceMonitor';
// 获取环境
const environment = import.meta.env.VITE_ENV
const app = createApp(App)
// 正式环境启用
if (environment === 'pro') {
app.use(performanceMonitor);
}

(3)结果展示:

Web极致性能优化指南

17.3:总结:

通过以上使用这些 API,我们可以获取到更详细的性能数据,我们可以借助这些数据对我的项目有一个较为全面的认识。到底哪里才是我们性能的瓶颈,我们优化应该从何入手。优化后是否有效,我们可以持续的跟踪结果得到正向反馈。

补充:

有的时候我们希望能看到站点的实际表现如何,抛开数据抛开一切代码。那么其实谷歌也提供了非常好的应用工具供我们开发使用。

控制台-Lighthouse

如下:
Web极致性能优化指南

我们可以看到很多关键数据,并且对于可优化的项目也会给出实际的操作建议。并且我们在实际项目中也大量使用该工具。

18:总结

Web前端runtime优化对于提升用户体验、降低成本、提高搜索引擎排名等方面有着重要的意义,是Web网站和应用开发中不可忽视的重要环节。通过合理的前端优化策略和技术手段,可以为用户提供更好的访问体验,提高网站的竞争力和价值。Web前端优化不仅是一项技术工作,更是一种态度和思维方式。不断思考和探索,才能在走得更远。

原文链接:https://juejin.cn/post/7338614904393367603 作者:Running_slave

(0)
上一篇 2024年2月24日 下午4:05
下一篇 2024年2月24日 下午4:16

相关推荐

发表回复

登录后才能评论