NodeJS 速習チュートリアル

Node.js Zlib モジュール

1. Zlib モジュール入門

Zlib モジュールは、zlib および brotli 圧縮ライブラリへのバインディングを提供します。これにより、以下の機能が実現可能です:

  • ファイルやデータストリームの圧縮および解凍
  • HTTP 圧縮の実装
  • 圧縮ファイル形式(.gz, .zip)の操作
  • Web アプリケーションにおける帯域幅利用の最適化

2. Zlib モジュールのインポート

const zlib = require('zlib');

3. 圧縮方式

Zlib モジュールは複数の圧縮方式をサポートしています:

メソッド説明
Gzip/Gunzip最も広く使われている圧縮形式。特に Web コンテンツで一般的。
Deflate/Inflateヘッダーやチェックサムを含まない生の Deflate アルゴリズム。
DeflateRaw/InflateRawカスタムヘッダーやチェックサム処理を行うための生の Deflate アルゴリズム。
Brotliより高い圧縮率を提供するモダンなアルゴリズム(Node.js 10.16.0 で追加)。

4. 基本的な圧縮と解凍

4.1 コールバックの利用

const zlib = require('zlib');

const input = 'これは Node.js の zlib モジュールを使用して圧縮されるテキストです。';

// gzip を使用してデータを圧縮
zlib.gzip(input, (err, compressed) => {
  if (err) {
    console.error('圧縮エラー:', err);
    return;
  }
  
  console.log('元のサイズ:', input.length, 'バイト');
  console.log('圧縮後のサイズ:', compressed.length, 'バイト');
  console.log('圧縮率:', Math.round(100 - (compressed.length / input.length * 100)) + '%');
  
  // データを解凍
  zlib.gunzip(compressed, (err, decompressed) => {
    if (err) {
      console.error('解凍エラー:', err);
      return;
    }
    
    console.log('解凍されたデータ:', decompressed.toString());
    console.log('解凍成功:', input === decompressed.toString());
  });
});

4.2 プロミスの利用

const zlib = require('zlib');
const { promisify } = require('util');

// コールバックベースの関数をプロミスベースに変換
const gzipPromise = promisify(zlib.gzip);
const gunzipPromise = promisify(zlib.gunzip);

async function compressAndDecompress(input) {
  try {
    // 圧縮
    const compressed = await gzipPromise(input);
    console.log('元のサイズ:', input.length, 'バイト');
    console.log('圧縮後のサイズ:', compressed.length, 'バイト');
    
    // 解凍
    const decompressed = await gunzipPromise(compressed);
    console.log('解凍されたデータ:', decompressed.toString());
    console.log('成功:', input === decompressed.toString());
    
    return compressed;
  } catch (err) {
    console.error('エラー:', err);
  }
}

// 使用例
const testData = 'これは zlib モジュールで圧縮されるテストデータです。';
compressAndDecompress(testData);

5. ストリームの利用

Zlib モジュールをストリームと組み合わせることで、大容量のファイルやデータを効率的に処理できます:

const zlib = require('zlib');
const fs = require('fs');
const path = require('path');

// ファイルを圧縮
function compressFile(inputPath) {
  const outputPath = inputPath + '.gz';
  
  // 読み込みストリームと書き込みストリームを作成
  const input = fs.createReadStream(inputPath);
  const output = fs.createWriteStream(outputPath);
  
  // gzip 変換ストリームを作成
  const gzip = zlib.createGzip();
  
  // データを圧縮ストリーム経由でパイプ(pipe)
  input.pipe(gzip).pipe(output);
  
  // イベントのハンドリング
  input.on('error', (err) => console.error('入力エラー:', err));
  gzip.on('error', (err) => console.error('圧縮エラー:', err));
  output.on('error', (err) => console.error('出力エラー:', err));
  
  output.on('finish', () => {
    console.log(`ファイルの圧縮が完了しました: ${outputPath}`);
    
    // サイズ比較のための統計取得
    const inputStats = fs.statSync(inputPath);
    const outputStats = fs.statSync(outputPath);
    
    console.log(`元のサイズ: ${inputStats.size} バイト`);
    console.log(`圧縮後のサイズ: ${outputStats.size} バイト`);
    console.log(`圧縮率: ${Math.round(100 - (outputStats.size / inputStats.size * 100))}%`);
  });
}

// ファイルを解凍
function decompressFile(inputPath) {
  // 出力パスから .gz 拡張子を除去
  const outputPath = inputPath.endsWith('.gz')
    ? inputPath.slice(0, -3)
    : inputPath + '.uncompressed';
  
  // ストリームを作成
  const input = fs.createReadStream(inputPath);
  const output = fs.createWriteStream(outputPath);
  const gunzip = zlib.createGunzip();
  
  // データを解凍ストリーム経由でパイプ
  input.pipe(gunzip).pipe(output);
  
  // イベントのハンドリング
  input.on('error', (err) => console.error('入力エラー:', err));
  gunzip.on('error', (err) => console.error('解凍エラー:', err));
  output.on('error', (err) => console.error('出力エラー:', err));
  
  output.on('finish', () => {
    console.log(`ファイルの解凍が完了しました: ${outputPath}`);
  });
}

// 使用例 (テキストファイルが存在する場合)
// compressFile('example.txt');
// decompressFile('example.txt.gz');

console.log('この例ではストリームを使用してファイルを圧縮・解凍する方法を示しています。');5. ストリームの利用
Zlib モジュールをストリームと組み合わせることで、大容量のファイルやデータを効率的に処理できます:

       注意: ストリームを使用すると、ファイル全体を一度にメモリにロードする必要がないため、巨大なファイルを扱う際にメモリ効率が非常に高くなります。

6. HTTP 圧縮の実装

Zlib モジュールは、帯域幅を節約するための HTTP 圧縮に頻繁に使用されます:

const http = require('http');
const zlib = require('zlib');

// 圧縮機能を備えた HTTP サーバーを作成
const server = http.createServer((req, res) => {
  // サンプルのレスポンスコンテンツ
  const responseBody = `
    <!DOCTYPE html>
    <html>
    <head>
      <title>Zlib 圧縮の例</title>
    </head>
    <body>
      <h1>Zlib による HTTP 圧縮</h1>
      <p>このコンテンツはブラウザに送信される前に Gzip で圧縮されています。</p>
      <p>圧縮により帯域幅が節約され、ページのロード時間が改善されます。</p>
      ${'<p>この段落は圧縮効率を実演するために繰り返されています。</p>'.repeat(50)}
    </body>
    </html>
  `;
  
  // クライアントが gzip エンコーディングを受け入れるか確認
  const acceptEncoding = req.headers['accept-encoding'] || '';
  
  // コンテンツタイプを設定
  res.setHeader('Content-Type', 'text/html');
  
  // クライアントがサポートしている場合、レスポンスを圧縮
  if (/\bgzip\b/.test(acceptEncoding)) {
    // クライアントが gzip をサポート
    res.setHeader('Content-Encoding', 'gzip');
    
    // 圧縮して送信
    zlib.gzip(responseBody, (err, compressed) => {
      if (err) {
        res.statusCode = 500;
        res.end('Internal Server Error');
        return;
      }
      
      res.end(compressed);
    });
  } else if (/\bdeflate\b/.test(acceptEncoding)) {
    // クライアントが deflate をサポート
    res.setHeader('Content-Encoding', 'deflate');
    
    // 圧縮して送信
    zlib.deflate(responseBody, (err, compressed) => {
      if (err) {
        res.statusCode = 500;
        res.end('Internal Server Error');
        return;
      }
      
      res.end(compressed);
    });
  } else {
    // 圧縮をサポートしていない場合
    res.end(responseBody);
  }
});

// ポート 8080 でサーバーを起動
const PORT = 8080;
server.listen(PORT, () => {
  console.log(`サーバーが http://localhost:${PORT}/ で稼働中です`);
});

7. Brotli 圧縮の利用

Brotli はモダンな圧縮アルゴリズムであり、多くの場合 Gzip よりも優れた圧縮率を実現します:

const zlib = require('zlib');

// 圧縮するサンプルデータ
const input = '比較のために異なるアルゴリズムで圧縮されるテストデータです。'.repeat(20);

// 圧縮メソッドの比較
function compareCompression() {
  console.log(`元のデータサイズ: ${input.length} バイト`);
  
  // Gzip 圧縮
  zlib.gzip(input, (err, gzipped) => {
    if (err) {
      console.error('Gzip エラー:', err);
      return;
    }
    
    console.log(`Gzip サイズ: ${gzipped.length} バイト (${Math.round(100 - (gzipped.length / input.length * 100))}% 削減)`);
    
    // Deflate 圧縮
    zlib.deflate(input, (err, deflated) => {
      if (err) {
        console.error('Deflate エラー:', err);
        return;
      }
      
      console.log(`Deflate サイズ: ${deflated.length} バイト (${Math.round(100 - (deflated.length / input.length * 100))}% 削減)`);
      
      // Brotli 圧縮 (利用可能な場合)
      if (typeof zlib.brotliCompress === 'function') {
        zlib.brotliCompress(input, (err, brotli) => {
          if (err) {
            console.error('Brotli エラー:', err);
            return;
          }
          
          console.log(`Brotli サイズ: ${brotli.length} バイト (${Math.round(100 - (brotli.length / input.length * 100))}% 削減)`);
        });
      } else {
        console.log('この Node.js バージョンでは Brotli 圧縮は利用できません');
      }
    });
  });
}

// 比較を実行
compareCompression();

       注意: Brotli 圧縮は Node.js 10.16.0 以降で利用可能です。通常、Gzip よりも高い圧縮率を実現しますが、圧縮処理自体は Gzip よりも低速になる場合があります。

8. 圧縮オプション

オプションを使用して圧縮の挙動をカスタマイズできます:

const zlib = require('zlib');

const input = 'カスタムオプション付きの圧縮用サンプルコンテンツです。'.repeat(50);

// 圧縮レベルのテスト
function testCompressionLevels() {
  console.log(`元のサイズ: ${input.length} バイト`);
  
  // デフォルト圧縮 (レベル 6)
  zlib.gzip(input, (err, compressed) => {
    if (err) throw err;
    console.log(`デフォルト圧縮 (レベル 6): ${compressed.length} バイト`);
    
    // 最速圧縮 (レベル 1)
    zlib.gzip(input, { level: 1 }, (err, fastCompressed) => {
      if (err) throw err;
      console.log(`最速圧縮 (レベル 1): ${fastCompressed.length} バイト`);
      
      // 最高圧縮 (レベル 9)
      zlib.gzip(input, { level: 9 }, (err, bestCompressed) => {
        if (err) throw err;
        console.log(`最高圧縮 (レベル 9): ${bestCompressed.length} バイト`);
      });
    });
  });
}

// カスタムメモリ使用量のテスト
function testMemoryLevels() {
  // メモリレベル: 1 (最小) から 9 (最大)
  zlib.gzip(input, { memLevel: 9 }, (err, compressed) => {
    if (err) throw err;
    console.log(`高いメモリ使用量 (memLevel 9): ${compressed.length} バイト`);
    
    zlib.gzip(input, { memLevel: 4 }, (err, lowMemCompressed) => {
      if (err) throw err;
      console.log(`低いメモリ使用量 (memLevel 4): ${lowMemCompressed.length} バイト`);
    });
  });
}

// テストを実行
testCompressionLevels();
setTimeout(testMemoryLevels, 1000);

一般的な Zlib オプション:

  • level: 圧縮レベル (0-9。0 は無圧縮、9 は最高圧縮)
  • memLevel: メモリ使用量 (1-9。1 は最小、9 は最大)
  • strategy: 圧縮戦略 (例: Z_DEFAULT_STRATEGY)
  • dictionary: 圧縮用の事前定義された辞書
  • windowBits: ウィンドウサイズの対数

9. エラーハンドリング

圧縮・解凍を扱う際、適切なエラーハンドリングは極めて重要です:

const zlib = require('zlib');

// データを安全に解凍する関数
function safeDecompress(compressedData) {
  return new Promise((resolve, reject) => {
    zlib.gunzip(compressedData, { finishFlush: zlib.constants.Z_SYNC_FLUSH }, (err, result) => {
      if (err) {
        // 特定のエラータイプを処理
        if (err.code === 'Z_DATA_ERROR') {
          reject(new Error('無効または破損した圧縮データです'));
        } else if (err.code === 'Z_BUF_ERROR') {
          reject(new Error('不完全な圧縮データです'));
        } else {
          reject(err);
        }
        return;
      }
      
      resolve(result);
    });
  });
}

// エラーハンドリング付きの使用例
async function demonstrateErrorHandling() {
  try {
    // 正当な圧縮
    const validData = zlib.gzipSync('これは有効なデータです');
    console.log('有効なデータの圧縮に成功しました');
    
    // 解凍を試行
    const result = await safeDecompress(validData);
    console.log('解凍成功:', result.toString());
    
    // 無効なデータの解凍を試行
    const invalidData = Buffer.from('これは圧縮データではありません');
    await safeDecompress(invalidData);
    
  } catch (err) {
    console.error('エラーが発生しました:', err.message);
  }
}

demonstrateErrorHandling();

10. 実践的なユースケース

10.1 ログファイルの圧縮

const zlib = require('zlib');
const fs = require('fs');
const path = require('path');

// ログファイルを圧縮してタイムスタンプを付与
function compressLogFile(logFilePath) {
  const timestamp = new Date().toISOString().replace(/:/g, '-');
  const basename = path.basename(logFilePath);
  const outputPath = path.join(
    path.dirname(logFilePath),
    `${basename}-${timestamp}.gz`
  );
  
  const input = fs.createReadStream(logFilePath);
  const output = fs.createWriteStream(outputPath);
  const gzip = zlib.createGzip();
  
  input.pipe(gzip).pipe(output);
  
  output.on('finish', () => {
    console.log(`ログファイルが圧縮されました: ${outputPath}`);
    
    // オプションで元のログファイルをクリア
    fs.writeFile(logFilePath, '', err => {
      if (err) console.error(`クリアエラー: ${err.message}`);
    });
  });
}

10.2 API レスポンスの圧縮

const http = require('http');
const zlib = require('zlib');

const apiData = {
  users: Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`,
    profile: {
      bio: `これはユーザー ${i + 1} のサンプルバイオです。圧縮を実演するためのテキストを含みます。`
    }
  }))
};

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/api/users') {
    const jsonData = JSON.stringify(apiData);
    const acceptEncoding = req.headers['accept-encoding'] || '';
    
    res.setHeader('Content-Type', 'application/json');
    
    if (/\bgzip\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'gzip');
      zlib.gzip(jsonData, (err, compressed) => {
        if (err) {
          res.statusCode = 500;
          res.end(JSON.stringify({ error: 'Compression failed' }));
          return;
        }
        res.end(compressed);
      });
    } else {
      res.end(jsonData);
    }
  }
});

server.listen(8080);

11. 高度な圧縮テクニック

11.1 圧縮戦略 (Strategies)

Zlib は特定のデータタイプに対してより効果的な、異なる圧縮戦略を提供します:

const zlib = require('zlib');

const repeatedData = 'ABC'.repeat(1000);

function testStrategies(data) {
  const strategies = [
    { name: 'DEFAULT_STRATEGY', value: zlib.constants.Z_DEFAULT_STRATEGY },
    { name: 'FILTERED', value: zlib.constants.Z_FILTERED },
    { name: 'HUFFMAN_ONLY', value: zlib.constants.Z_HUFFMAN_ONLY },
    { name: 'RLE', value: zlib.constants.Z_RLE },
    { name: 'FIXED', value: zlib.constants.Z_FIXED }
  ];
  
  strategies.forEach(({ name, value }) => {
    const compressed = zlib.gzipSync(data, { strategy: value });
    console.log(`${name.padEnd(20)}: ${compressed.length} バイト`);
  });
}

testStrategies(repeatedData);

11.2 カスタム辞書 (Custom Dictionaries)

特定のデータパターンに対して、カスタム辞書を使用すると圧縮率を劇的に向上させることができます:

const zlib = require('zlib');

// よく使われる用語を含むカスタム辞書を作成
const dictionary = Buffer.from('username,password,email,first_name,last_name,status,admin,user,role');

const userData = JSON.stringify({
  username: 'johndoe',
  email: '[email protected]',
  first_name: 'John',
  last_name: 'Doe',
  role: 'admin',
  status: 'active'
});

// 辞書あり・なしで圧縮
const compressedWithout = zlib.deflateSync(userData);
const compressedWith = zlib.deflateSync(userData, { dictionary });

console.log('辞書なしサイズ:', compressedWithout.length, 'バイト');
console.log('辞書ありサイズ:', compressedWith.length, 'バイト');
console.log('改善率:', Math.round((1 - (compressedWith.length / compressedWithout.length)) * 100) + '%');

// 辞書を使用して解凍
const decompressed = zlib.inflateSync(compressedWith, { dictionary });

12. パフォーマンスに関する考慮事項

  • 圧縮レベルのトレードオフ: レベルを上げると圧縮率は良くなりますが、処理時間は長くなります。
  • メモリ使用量: 圧縮処理は特に高レベルにおいてメモリを大量に消費する可能性があります。
  • 圧縮のタイミング: 圧縮によるメリットがあるデータ(テキスト、JSON など)のみを圧縮してください。
  • 既存の圧縮データ: すでに圧縮されているファイル(画像、動画など)を再度圧縮しないでください(サイズがかえって増えることがあります)。
  • スレッドプール: Zlib 操作は libuv のスレッドプールを使用します。必要に応じて UV_THREADPOOL_SIZE を調整してください。

13. まとめ

Node.js の Zlib モジュールは、以下の目的で不可欠な機能を提供します:

  • ファイルサイズと帯域幅使用量の削減
  • 圧縮フォーマットの効率的な操作
  • HTTP レスポンスの最適化
  • ストリームによる大規模データ処理

複数のアルゴリズム(Gzip, Deflate, Brotli)のサポートと、同期・非同期・ストリームの各 API を理解することで、Node.js アプリケーションのデータ転送とストレージを最大限に最適化することが可能になります。