如何使用 JavaScript 将 localStorage 空间填满?

本周我需要测试js将数据存储到空间已满的localStorage中的表现,为此,我想找到一种使用 JavaScript 将 localStorage 空间填满的方法。

我们知道检测 localStorage 何时已满的唯一方法是在调用 localStorage.setItem 时捕获 QuotaExceededError ,我最初的想法是编写一个循环,逐字节增加 localStorage 大小直到它已满,例如:

try {
  for (let i = 1; i < Infinity; i++) {
    localStorage.setItem("__test__", "x".repeat(i));
  }
} catch (err) {}

但是,逐字节写入并不是一个好的选择。它的效率非常低,很容易使浏览器崩溃。

因此,受这个 StackOverflow 答案的启发,我选择先存储较大的字符串,然后回退到较小的字符串,直到 localStorage 已满:

/**
 * Fill localStorage to its maximum capacity.
 *
 * First, we fill localStorage in chunks of 100.000 characters until it
 * hits the exceeded quota exception.
 * Then, we do it again with chunks of 1.000 characters.
 * Finally, we do it again character by character, to ensure localStorage is
 * completely full.
 *
 * To cleanup localStorage you can use localStorage.clear(). Still, just in case
 * you wanted to clean up only the data stored by this function (maybe because
 * you want to keep in the localStorage the stuff you stored before running it),
 * we return a convenient cleanup function.
 *
 * @return A cleanup function to delete the data we stored.
 */
function fillLocalStorage(): () => void {
  function storeIncreasinglyBigItem(
    key: string,
    charactersIncrement: number
  ): void {
    const MAX_ITERATIONS = 10_000; // Safeguard against OOM & crashes
    for (let i = 1; i <= MAX_ITERATIONS; i++) {
      localStorage.setItem(key, "x".repeat(i * charactersIncrement));
    }
  }
  try {
    storeIncreasinglyBigItem("__1", 100_000);
  } catch (_err1) {
    try {
      storeIncreasinglyBigItem("__2", 1_000);
    } catch (_err2) {
      try {
        storeIncreasinglyBigItem("__3", 1);
      } catch (_err3) {
        // Storage is now completely full 🍟
      }
    }
  }
  return function cleanup() {
    localStorage.removeItem("__1");
    localStorage.removeItem("__2");
    localStorage.removeItem("__3");
  };
}

请注意,这个解决方案有一个很小的边缘情况,其中 fillLocalStorage 没有 100% 填充 localStorage:由于浏览器包含确定 localStorage 大小的值和键,在极少数情况下,可能会报告存储空间已满但可能还剩下大约 3 个字节。

上面的片段肯定可以被优化,但效果还行,这就是我最初在这篇文章中建议的。到我在 Reddit 上分享了它,其中 u/kyle1320/u/palparepa提出了一种更优雅的方法:先找到最高阶的位,然后以递减的方式测试每个位,直到localStorage满了为止。

这种方法比我上面建议的方法性能更高,因为它涉及更少的分配和迭代,它使用单个 localStorage 项,并且还解决了我刚才提到的边缘情况。

/**
 * Fill localStorage to its maximum capacity.
 *
 * First, we find the highest order bit (the bit with the highest place value)
 * that fits in localStorage by storing an increasingly big string
 * of length 2, 4, 8, 16, etc. until it won't fill localStorage anymore.
 *
 * Then, we fill the remaining space by increasing the string length
 * in the opposite order.
 *
 * By working in iterations, starting with very long strings, and storing data
 * in different items, we can keep a low memory profile and reduce the number of
 * writes — making this process pretty fast.
 *
 * To cleanup localStorage you can use localStorage.clear(). Still, just in case
 * you wanted to clean up only the data stored by this function (maybe because
 * you want to keep in the localStorage the stuff you stored before running it),
 * we return a convenient cleanup function.
 *
 * @return A cleanup function to delete the data we stored.
 */
function fillLocalStorage(): () => void {
  const key = "__filling_localstorage__";

  let max = 1; // This holds the highest order bit.
  let data = "x"; // The string we're going to store in localStorage.

  // Find the highest order bit.
  try {
    while (true) {
      data = data + data;
      localStorage.setItem(key, data);
      max <<= 1;
    }
  } catch {}

  // Fill the rest of the space by increasing the string length in the opposite
  // order.
  for (let bit = max >> 1; bit > 0; bit >>= 1) {
    try {
      localStorage.setItem(key, data.substring(0, max | bit));
      max |= bit;
    } catch {
      // Storage is now completely full 🍟
    }
  }

  // Cleanup
  return function cleanup() {
    localStorage.removeItem(key);
  };
}

示例用法:

// Fill localStorage.
const cleanupLocalStorage = fillLocalStorage();
// The localStorage is full now. Do whatever you need to.
doSomething();
// Finally, delete the dummy data we used to fill localStorage.
cleanupLocalStorage();

PuppeteerPlaywright 中的示例用法:

test.afterEach(async ({ page }) => {
  await page.evaluate(() => window.localStorage.clear());
});

test("Do something when localStorage is full", async ({ page }) => {
  // Fill localStorage.
  // Notice that the page is running in a different JavaScript context, so
  // we serialize the "fillLocalStorage" utility (it's OK, it's a pure function)
  // and run an eval on the browser side.
  await page.evaluate(`(${fillLocalStorage.toString()})()`);

  // The localStorage is full now. Do whatever you need to.
  doSomething();
});

我用 chrome 测试了下,他的 localStorage 空间是5M。

引用链接

[1] StackOverflow 答案: https://stackoverflow.com/a/45760532
[2] u/kyle1320: https://old.reddit.com/user/kyle1320
[3] /u/palparepa: https://old.reddit.com/user/palparepa
[4] Puppeteer: https://github.com/puppeteer/puppeteer
[5] Playwright: https://github.com/microsoft/playwright