Web极致性能优化指南(下)

1:写在前面

Web极致性能优化指南(上) 中我介绍了多种优化方案,在这篇文章中我将继续深入的介绍探讨优化方案和策略并给出足够详细的代码应用实例,以此实现对Web应用的极致优化。 废话不多说直接上干货!!!

2:使用 Web Worker

2.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、导致额外性能问题(在高负载下可能会使系统卡顿)、交互复杂等。

3: 动画

3.1:介绍

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

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

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

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

3.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

4:代码流程控制

4.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 解构。

6:减少重绘重排

6.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>

6.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>

6.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>

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

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

6.5:使用 requestAnimationFrame

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

6.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);
 
// 思路就是创建文档碎片,用变量存操作,最后操作完毕统一交给浏览器渲染

6.7:优化图片和媒体资源

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

7:计算样式

7.1:介绍

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

7.2:实例

js 复制代码
<!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对象,可以获取到元素的所有计算样式,包括外部样式表和内联样式。

8:内存管理

8.1:介绍:

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

8.2:使用

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

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

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

js 复制代码
<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)移除监听

(3)减少dom渲染

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

(4)打印移除

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

(5)意外的环形引用

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

(6)被忽略的闭包

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

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

js 复制代码
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,对于垃圾回收算法就能很快进行回收动作避免泄露。

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

js 复制代码
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 看似已经毫无关系,认知上这好像也毫无问题。但实际上,内存还是被保留了下来,往往这种代码容易让我们掉以轻心。
// 在我们的代码中经常出现这种结构,稍不注意就会掉坑里。

9:缓存

9.1:http缓存

1:缓存设置:

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

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

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

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

js 复制代码
Cache-Control: max-age=3600

(2)Expires

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

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

js 复制代码
Expires: Sat, 31 Dec 2025 23:59:59 GMT

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

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

例如:

js 复制代码
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 状态码。

例如:

js 复制代码
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 中的资源指纹也会随之变化,从而迫使浏览器重新下载资源。通常我们在构建工具中做相应的配置即可。

(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 缓存策略,例如在构建时为静态资源添加哈希值,并将哈希值作为文件名,从而实现缓存的版本控制。

如下:

js 复制代码
// 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 头来实现缓存控制。

如下:

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

9.2:本地缓存

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

如下:

(1)Cookie

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

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

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

(2)Web Storage

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

特点

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

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

(3)IndexedDB

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

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

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

js 复制代码
// 打开数据库
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)总结

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

10:性能指标监控

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

(1)获取数据

(2)计算结果

10.1:获取数据

我们先看一张图:

简而言之我们需要拿到这些数据,我们可以通过window.performance.timing拿到相关数据,如下:

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

  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 事件完成的时间戳,此时页面的所有资源加载和执行都已经完成,页面已经完全加载。

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

10.2:计算结果

(1)计算代码:

js 复制代码
// 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)项目实现:

js 复制代码
// 插件
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)
    }
  }
}
js 复制代码
// 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)结果展示:

10.3:总结:

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

补充:

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

控制台-Lighthouse

如下:

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

11:总结

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

相关推荐
木子七3 分钟前
vue3-setup中使用响应式
前端·vue
廖子默13 分钟前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
Moment16 分钟前
毕业半年,终于拥有了两个近 500 star 的开源项目了 🤭🤭🤭
前端·后端·开源
光影少年1 小时前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei1 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑1 小时前
深入理解 JavaScript 中的 AbortController
前端·javascript
摸鱼也很难1 小时前
解决 node.js 执行 npm下载 报无法执行脚本的错
前端·npm·node.js
换个名字不能让人发现我在摸鱼1 小时前
裁剪保存的图片黑边问题
前端·javascript
PeterJXL1 小时前
pnpm:包管理的新星,平替 npm 和 yarn
前端·npm·node.js·pnpm