目次
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: |
# デプロイ処理
ビルド最適化の成果
実装した最適化により、以下の改善を実現しました:
- ビルド時間: 120秒 → 45秒(62.5%削減)
- 生成ページ数: 30ページ → 14ページ(53%削減)
- ビルドサイズ: 15MB → 8MB(46.7%削減)
- 不要コンテンツ: 16件の非研究コンテンツを除外
まとめ
Astroでの効率的なビルド最適化は、以下のポイントが重要です:
- データ駆動型のページ生成 - 実際のデータに基づいてページを生成
- ビルド時のフィルタリング - 不要なコンテンツを早期に除外
- インクリメンタルビルド - 変更部分のみを処理
- 並列処理の活用 - 複数の処理を同時実行
- 適切なキャッシュ戦略 - ビルド時間とデータ鮮度のバランス
これらのテクニックを組み合わせることで、大規模なサイトでも高速なビルドと効率的なデータ管理が可能になります。プロジェクトの特性に応じて、適切な最適化手法を選択し、実装することが重要です。