Astroで実装する効率的なビルド最適化とデータフィルタリング

公開日:
目次

Astro.jsを使った静的サイト生成では、ビルド時のデータ処理が重要です。特に、外部データソースから取得したデータを効率的にフィルタリングし、不要なページ生成を防ぐことで、ビルド時間の短縮とサイトのパフォーマンス向上が実現できます。本記事では、実践的な最適化テクニックを紹介します。

動的ルーティングの最適化

Astroの動的ルーティングでは、getStaticPaths()関数でページを生成しますが、データ駆動型のアプローチを採用することで、無駄なページ生成を防げます。

問題:空のページが生成される

// 問題のある実装
export async function getStaticPaths() {
  // 30日分のページを機械的に生成
  const paths = [];
  const today = new Date();
  
  for (let i = 0; i < 30; i++) {
    const date = new Date(today);
    date.setDate(today.getDate() - i);
    paths.push({
      params: { date: formatDate(date) }
    });
  }
  
  return paths;
}

この実装では、データの有無に関わらず30日分のページが生成されてしまいます。

解決:データ駆動型の実装

// 改善された実装
import data from '../data/articles.json';

export async function getStaticPaths() {
  // 実際にデータが存在する日付のみ抽出
  const uniqueDates = new Set();
  
  data.articles.forEach(article => {
    const date = formatDate(new Date(article.publishedDate));
    uniqueDates.add(date);
  });
  
  // データが存在する日付のみページを生成
  const paths = Array.from(uniqueDates).map(date => ({
    params: { date }
  }));
  
  console.log(`生成ページ数: ${paths.length}(元: 30ページ)`);
  
  return paths;
}

この改善により、30ページから14ページへ、53%の削減を実現できました。

ビルド時のデータフィルタリング

RSSフィードの不要なコンテンツ除外

外部のRSSフィードから取得したデータには、記事以外のコンテンツが含まれることがあります。

function filterResearchArticles(articles) {
  const nonResearchPatterns = [
    /^Front Cover:/i,
    /^Inside (Back|Front) Cover:/i,
    /^Issue Editorial Masthead$/i,
    /^Correction to /i,
    /^Author Correction:/i,
    /^Publisher Correction:/i,
    /^Erratum:/i,
    /^Contents:/i,
    /^Graphical Contents/i
  ];
  
  return articles.filter(article => {
    // タイトルベースのフィルタリング
    const isNonResearch = nonResearchPatterns.some(pattern => 
      pattern.test(article.title)
    );
    
    if (isNonResearch) {
      console.log(`除外: ${article.title}`);
      return false;
    }
    
    // カテゴリベースのフィルタリング
    if (article.category && 
        ['correction', 'erratum', 'masthead'].includes(
          article.category.toLowerCase()
        )) {
      return false;
    }
    
    return true;
  });
}

// 使用例
const rawArticles = await fetchRSSData();
const filteredArticles = filterResearchArticles(rawArticles);
console.log(`フィルタリング結果: ${rawArticles.length}${filteredArticles.length}`);

特定サイトのコンテンツクリーニング

サイト固有の問題に対応するための処理も重要です。

function cleanArticleContent(article, source) {
  switch(source) {
    case 'nature':
      return cleanNatureContent(article);
    case 'science':
      return cleanScienceContent(article);
    default:
      return article;
  }
}

function cleanNatureContent(article) {
  // Nature系のメタデータ除去
  const metadataPattern = /^Nature.*?,\s*Published online:.*?doi:10\.1038\/.*?\s*/;
  
  if (article.abstract) {
    article.abstract = article.abstract
      .replace(metadataPattern, '')
      .trim();
  }
  
  return article;
}

ビルドパフォーマンスの最適化

1. インクリメンタルビルド戦略

// scripts/incremental-build.js
import fs from 'fs';
import path from 'path';

async function incrementalBuild() {
  const buildCache = loadBuildCache();
  const currentData = await fetchCurrentData();
  
  // 変更があったデータのみ処理
  const changedItems = currentData.filter(item => {
    const cached = buildCache[item.id];
    return !cached || cached.hash !== generateHash(item);
  });
  
  console.log(`変更検出: ${changedItems.length}/${currentData.length}`);
  
  // 変更があった部分のみ再ビルド
  for (const item of changedItems) {
    await processItem(item);
    buildCache[item.id] = {
      hash: generateHash(item),
      lastBuilt: new Date().toISOString()
    };
  }
  
  saveBuildCache(buildCache);
}

2. 並列処理の活用

// データ処理の並列化
async function processDataInParallel(items, batchSize = 5) {
  const results = [];
  
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    results.push(...batchResults);
    
    // 進捗表示
    console.log(`処理進捗: ${Math.min(i + batchSize, items.length)}/${items.length}`);
  }
  
  return results;
}

3. キャッシュ戦略

// Astroの実験的機能を活用
// astro.config.mjs
export default defineConfig({
  experimental: {
    contentCollectionCache: true,
    // キャッシュディレクトリの指定
    cacheDir: './.astro-cache'
  }
});

実践的なビルドスクリプト

package.jsonの設定

{
  "scripts": {
    "build": "npm run fetch-data && npm run filter-data && astro build",
    "build:fast": "npm run build --experimental-static-build",
    "build:analyze": "npm run build -- --analyze",
    "fetch-data": "node scripts/fetch-data.js",
    "filter-data": "node scripts/filter-data.js",
    "check-freshness": "node scripts/check-data-freshness.js"
  }
}

データ鮮度チェック

// scripts/check-data-freshness.js
import data from '../src/data/articles.json' assert { type: 'json' };

function checkDataFreshness() {
  const lastUpdated = new Date(data.lastUpdated);
  const now = new Date();
  const hoursSinceUpdate = (now - lastUpdated) / (1000 * 60 * 60);
  
  if (hoursSinceUpdate > 12) {
    console.warn(`⚠️  データが${Math.floor(hoursSinceUpdate)}時間前のものです`);
    console.warn('最新データの取得を推奨します: npm run fetch-data');
    return false;
  }
  
  console.log(`✅ データは${Math.floor(hoursSinceUpdate)}時間前に更新されています`);
  return true;
}

// CI/CDで使用
if (!checkDataFreshness() && process.env.CI) {
  process.exit(1);
}

GitHub Actionsとの連携

定期的なデータ更新とビルド

name: Update and Build

on:
  schedule:
    - cron: '0 */6 * * *'  # 6時間ごと
  workflow_dispatch:

jobs:
  update-and-build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Fetch latest data
      run: npm run fetch-data
      env:
        API_KEY: ${{ secrets.API_KEY }}
    
    - name: Filter and process data
      run: npm run filter-data
    
    - name: Check data changes
      id: check
      run: |
        if git diff --quiet src/data/; then
          echo "changed=false" >> $GITHUB_OUTPUT
        else
          echo "changed=true" >> $GITHUB_OUTPUT
        fi
    
    - name: Build site
      if: steps.check.outputs.changed == 'true'
      run: npm run build
    
    - name: Deploy
      if: steps.check.outputs.changed == 'true'
      run: |
        # デプロイ処理

ビルド最適化の成果

実装した最適化により、以下の改善を実現しました:

  1. ビルド時間: 120秒 → 45秒(62.5%削減)
  2. 生成ページ数: 30ページ → 14ページ(53%削減)
  3. ビルドサイズ: 15MB → 8MB(46.7%削減)
  4. 不要コンテンツ: 16件の非研究コンテンツを除外

まとめ

Astroでの効率的なビルド最適化は、以下のポイントが重要です:

  1. データ駆動型のページ生成 - 実際のデータに基づいてページを生成
  2. ビルド時のフィルタリング - 不要なコンテンツを早期に除外
  3. インクリメンタルビルド - 変更部分のみを処理
  4. 並列処理の活用 - 複数の処理を同時実行
  5. 適切なキャッシュ戦略 - ビルド時間とデータ鮮度のバランス

これらのテクニックを組み合わせることで、大規模なサイトでも高速なビルドと効率的なデータ管理が可能になります。プロジェクトの特性に応じて、適切な最適化手法を選択し、実装することが重要です。