1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Puppeteer動的サイトスクレイピングのメモリリーク対策|AI実務ノート 編集部

1
Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

Puppeteerを使った動的サイトスクレイピングで必ず直面するメモリリーク問題の、実務で使える対策を全て網羅します。

  • Browser/Pageインスタンスの適切なライフサイクル管理
  • Event Listenerの確実なクリーンアップ
  • DOM参照の循環参照対策
  • 長時間稼働時のメモリ監視パターン

正直に言うと、僕も最初にPuppeteerを本格導入した時は「なんでこんなにメモリが増え続けるんだ…」と深夜まで格闘した経験があります。でも今思えば、基本的なリソース管理の原則さえ押さえておけば防げる問題でした。

2. 前提(環境・読者層)

  • Node.js v18+ / Puppeteer v21+
  • スクレイピング経験はあるが、メモリリークで困っている方
  • 本番環境で長時間稼働が必要なプロジェクト

想定読者:

  • 「開発環境では動くけど、本番で数時間後にクラッシュする」
  • 「Docker上で動かすと、いつの間にかメモリ使用量が1GB超えてる」
  • 「複数ページを巡回する処理で、なぜかどんどん重くなる」

こんな経験がある方なら、この記事で確実に解決できます。

3. Before:よくあるつまずきポイント

危険パターン1: Page インスタンスの使い回し

// ❌ 危険:同一Pageを使い回し
const browser = await puppeteer.launch();
const page = await browser.newPage();
 
for (let i = 0; i < 1000; i++) {
  await page.goto(`https://example.com/page/${i}`);
  // DOMの蓄積、Event Listenerの蓄積...
}

危険パターン2: Browser の使い回し過多

// ❌ 危険:無制限にページを開く
const browser = await puppeteer.launch();
 
for (let url of urls) {
  const page = await browser.newPage(); // ページが蓄積される
  await page.goto(url);
  // page.close() を忘れがち
}

危険パターン3: DOM参照の保持

// ❌ 危険:JSハンドルを解放しない
const elements = await page.$$('a');
// elementsを使った処理...
// elements.forEach(e => e.dispose()) を忘れる

なぜ危険か:
Puppeteerは内部でChromiumプロセスを管理していて、適切にクリーンアップしないと:

① DOMノードがメモリに残り続ける

② Event Listenerが蓄積される

③ 画像・CSS等のリソースキャッシュが増大する

④ 最終的にOOMキラーに殺される

4. After:基本的な解決パターン

解決パターン1: ページ単位でのクリーンアップ

// ✅ 安全:適切なリソース管理
async function scrapeSinglePage(browser, url) {
  const page = await browser.newPage();
 
  try {
    await page.goto(url, { waitUntil: 'networkidle0' });
    const data = await page.evaluate(() => {
      return document.title; // 必要なデータのみ抽出
    });
    return data;
  } finally {
    await page.close(); // 必ず実行される
  }
}

解決パターン2: Browser のバッチ再起動

// ✅ 安全:定期的なBrowser再起動
class ManagedBrowser {
  constructor(batchSize = 50) {
    this.batchSize = batchSize;
    this.processedCount = 0;
    this.browser = null;
  }
 
  async getBrowser() {
    if (!this.browser || this.processedCount >= this.batchSize) {
      await this.restart();
    }
    return this.browser;
  }
 
  async restart() {
    if (this.browser) {
      await this.browser.close();
    }
    this.browser = await puppeteer.launch({
      args: ['--no-sandbox', '--disable-dev-shm-usage']
    });
    this.processedCount = 0;
  }
}

解決パターン3: JSハンドルの適切な破棄

// ✅ 安全:JSハンドルの管理
async function extractLinks(page) {
  const elements = await page.$$('a');
  const links = [];
 
  try {
    for (const element of elements) {
      const href = await element.getProperty('href');
      links.push(await href.jsonValue());
      await href.dispose(); // プロパティハンドルを解放
    }
  } finally {
    // 全てのElementHandleを解放
    await Promise.all(elements.map(e => e.dispose()));
  }
 
  return links;
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?