【性能优化】减少内存占用,提高性能🚀🚀🚀

3,272 阅读6分钟

减少内存占用,为什么提高性能呢?

当浏览器使用过多的内存时,它会变得缓慢、卡顿,并且更容易崩溃。这会影响你的浏览体验和效率。因此,如果你能减少浏览器的内存占用,你的页面就会更加流畅和快速地运行。为了减少浏览器的内存占用,就不得不提到JavaScript垃圾回收机制。

JavaScript的垃圾回收机制是自动的,并且是由JavaScript引擎负责调度和执行的。在通常情况下,为了最大限度地利用CPU和内存资源,垃圾回收机制会根据一定的算法和策略,自动寻找应该回收的内存对象,并对其进行回收。

虽然我们不能手动控制垃圾回收机制的执行,但我们可以通过一些编码技巧,减少内存分配和使用,从而促使垃圾回收机制的执行,减少垃圾回收的性能开销,提高代码执行速度。

一、减少全局变量

全局变量不易被垃圾回收,全局变量通常被定义在顶层作用域,它们会一直存在于整个应用程序的生命周期中,即使函数执行完毕之后,全局变量仍然存在于内存中,尽量减少全局变量的使用,并使用局部变量和函数封装。

// 不推荐的写法,使用全局变量
const globalVar = {};

function foo() {
  globalVar.value = "hello";
}

// 推荐的写法,使用局部变量
function bar() {
  let localVar = {};
  localVar.value = "world";
}

二、及时释放不再使用的对象、变量和事件监听器

当你不再需要一个对象时,及时解除对它的引用,从而让垃圾回收器回收其内存。

// 不及时释放对象
let data = [1, 2, 3];

function processData() {
  // do something with data
}

setInterval(function() {
  processData();
}, 1000);

// 推荐写法
let obj = {};
// 执行操作
obj = null; // 释放内存占用 

let btn = document.getElementById("btn");
btn.addEventListener("click", function() {
  // 执行操作
  btn.removeEventListener("click", arguments.callee); // 及时解绑事件监听器
}););

let obj = {a: 1, b: 2};
// 执行操作
delete obj.a; // 删除不再需要的属性

三、减少使用递归或无限循环等会占用大量内存的函数

循环引用可能导致内存泄漏。虽然现代浏览器的垃圾回收机制可以处理循环引用,但最好避免产生循环引用,在编写代码时使用开发者工具定期检查内存是否泄漏,并修复相关问题。

// 不推荐的递归写法,容易出现栈溢出
function recursion(n) {
  if (n <= 1) {
    return n;
  }
  return recursion(n - 1) + recursion(n - 2);
}

// 推荐的非递归写法
function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  let prev = 0, curr = 1;
  for (let i = 2; i <= n; i++) {
    let temp = curr;
    curr = prev + curr;
    prev = temp;
  }
  return curr;
}

四、使用对象池等技术,避免过多的内存分配和回收。

对于频繁创建和销毁的对象,可以使用对象池来减少垃圾回收的开销。对象池是一种常用的内存优化技术,它可以减少对象创建和销毁的次数,从而提高应用程序的性能。对象池可以用于缓存需要经常创建和销毁的对象,例如DOM元素和XHR对象等。

// 创建一个XHR对象池
const xhrPool = [];

function getXhr() {
  if (xhrPool.length > 0) {
    return xhrPool.pop();
  } else {
    return new XMLHttpRequest();
  }
}

function releaseXhr(xhr) {
  xhrPool.push(xhr);
}

// 使用XHR对象池
const xhr = getXhr();
xhr.open("GET", "https://example.com");
xhr.onload = function() {
  // do something
  releaseXhr(xhr);
};
xhr.send();

在这个例子中,我们在全局作用域内定义了一个XHR对象池,其中存放已经被创建但暂时不再使用的XHR对象。我们创建了getXhrreleaseXhr两个函数,用于从XHR池中获取对象和将对象还回池中,当需要使用XHR对象时,我们先通过getXhr函数获取一个对象,执行完相应的操作后,再将对象还回对象池中,而不是直接将对象销毁。

这个技巧可以避免过多的内存分配和回收,从而提高性能。值得注意的是,对象池不是适用于所有情况的解决方案,使用对象池需要根据具体场景进行权衡和调整。

五、减少DOM的操作和避免频繁的重排和重绘,使用事件委托等技术避免大量的事件监听器堆积

<!-- 不推荐的写法,生成大量的事件监听器 -->
<ul>
  <li onclick="handleClick(0)">Item 1</li>
  <li onclick="handleClick(1)">Item 2</li>
  <li onclick="handleClick(2)">Item 3</li>
  <!-- ... -->
</ul>

<!-- 推荐的写法,使用事件委托 -->
<ul id="list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <!-- ... -->
</ul>

<script>
let list = document.getElementById("list");

function handleClick(e) {
  let index = Array.from(list.children).indexOf(e.target);
  // do something
}

list.addEventListener("click", handleClick);
</script>

六、合理使用计时器,减少计时器的执行频率,避免占用过多的CPU资源

在代码中合理使用 requestAnimationFramesetTimeout/setInterval 等异步操作,以让垃圾回收器在空闲时间内执行。

// 不推荐的写法,间隔时间过短,会频繁占用CPU
setInterval(function() {
  // do something
}, 100);

// 推荐的写法,间隔时间适当,避免占用过多的CPU
let timer = setInterval(function() {
  // do something
}, 1000);

// 当不再需要执行计时器时,及时清除计时器
clearInterval(timer); 

// 使用window.requestAnimationFrame()代替setInterval()或setTimeout(),
// 尽可能让浏览器在下一帧渲染前执行代码。
function animate() {
  // do something
  window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);

// 当不再需要执行动画时,及时停止requestAnimationFrame
window.cancelAnimationFrame(requestID);

七、减少使用闭包和匿名函数,减少内存占用和函数调用的开销

// 不推荐的写法,每次调用都会创建一个闭包对象
function foo() {
  let counter = 0;

  return function() {
    return ++counter;
  };
}

let bar = foo();

// 推荐的写法,将变量声明在函数外部
let counter = 0;

function add() {
  return ++counter;
}

let baz = add();

八、优化数据结构和算法

使用更高效的数据结构和算法可以降低内存使用,减少垃圾回收的频率。例如在处理大量数据和复杂任务时,优化数据结构和算法可以显著提高代码的性能和效率,以免导致内存占用过高。

常见的优化方法包括:

  1. 数组去重:采用ES6新特性Set进行去重,或者使用Hash表加快检索。
  2. 链表操作:针对链表的常用操作,如翻转、合并、删除等,应该采用高效的算法实现,以避免频繁的遍历和操作。
  3. 缓存计算结果:针对一些耗时的计算操作,可以将计算结果缓存下来,以便下次重复使用。
  4. 分治算法:针对一些复杂的计算任务,可以采用分治算法或者动态规划等高效算法策略,避免枚举和暴力搜索带来的性能瓶颈。
  5. 位运算优化:针对某些二进制位运算操作,采用位运算符号代替乘、除、取模等算数运算,可以提高计算性能。
  6. 搜索算法优化:针对搜索算法,采用剪枝、双向搜索等优化方法,可以大幅减少搜索空间,提高搜索效率。

以上几点只是JavaScript优化数据结构和算法的几个方面,JavaScript的算法空间很大,优化的思路也不只局限于以上的方法,随着web开发的发展,算法和框架也在不断发展,我们应该根据应用场景和实际需要不断适应。

下面给出一些JavaScript优化数据结构和算法的示例:

1.数组去重

let arr = [1, 2, 3, 1, 2, 4];
// 使用Set对象进行快速去重
let uniqueArr = Array.from(new Set(arr));
console.log(uniqueArr); // [1, 2, 3, 4]

// 或者使用Hash表进行去重
let hash = {};
let result = [];
for (let i = 0; i < arr.length; i++) {
  if (!hash[arr[i]]) {
    hash[arr[i]] = true;
    result.push(arr[i]);
  }
}
console.log(result); // [1, 2, 3, 4]

2.链表操作

class Node {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

// 翻转链表
function reverseList(head) {
  let prev = null, curr = head;
  while (curr) {
    let temp = curr.next;
    curr.next = prev;
    prev = curr;
    curr = temp;
  }
  return prev;
}

// 合并两个有序链表
function mergeList(l1, l2) {
  let dummyHead = new Node(null);
  let curr = dummyHead;
  while (l1 && l2) {
    if (l1.val < l2.val) {
      curr.next = l1;
      l1 = l1.next;
    } else {
      curr.next = l2;
      l2 = l2.next;
    }
    curr = curr.next;
  }
  curr.next = l1 ? l1 : l2;
  return dummyHead.next;
}

3.缓存计算结果

let cache = {};

function factorial(n) {
  if (n === 1 || n === 0) {
    return 1;
  }
  if (cache[n]) {
    return cache[n];
  }
  let result = n * factorial(n - 1);
  cache[n] = result;
  return result;
}

4.分治算法

// 归并排序
function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  let mid = arr.length >> 1;
  let left = arr.slice(0, mid);
  let right = arr.slice(mid);
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  let result = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }
  while (i < left.length) {
    result.push(left[i++]);
  }
  while (j < right.length) {
    result.push(right[j++]);
  }
  return result;
}

5.位运算优化

// 将数字除以2
function divideByTwo(n) {
  return n >> 1; // 相当于n / 2
}

// 判断奇偶性
function isEven(n) {
  return (n & 1) === 0; // 最右边一位为0即为偶数
}

6.搜索算法优化

// 遍历8皇后问题
function solveQueens(n) {
  let queens = new Array(n).fill(-1);
  let res = [];

  function backtrack(i) {
    if (i === n) {
      res.push([...queens]);
    } else {
      for (let j = 0; j < n; j++) {
        if (isValid(i, j)) {
          queens[i] = j;
          backtrack(i + 1);
          queens[i] = -1;
        }
      }
    }
  }

  function isValid(row, col) {
    for (let i = 0; i < row; i++) {
      if (queens[i] === col || 
          row - i === Math.abs(col - queens[i])) {
        return false;
      }
    }
    return true;
  }

  backtrack(0);
  return res;
}

以上这些示例只是JavaScript优化数据结构和算法的冰山一角,它们展示出了不同场景下的优化思路,相信可以帮助你进行自己的优化实践。

Tips: 在开发的时候我们可以使用performance.now()辅助性能检测,找出性能瓶颈,优化代码。

let startTime = performance.now();

// do something

let endTime = performance.now();

console.log("执行时间:", endTime - startTime);

虽然我们不能像C++那样控制垃圾回收,我们可以使用以上策略减少内存占用、减少垃圾回收的性能开销,提高代码执行速度。

请注意!!! 不同的JavaScript引擎可能具有不同的垃圾回收策略,因此实际效果可能稍有差别