SolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検

SolidJS アプリケーションを本番環境にデプロイする前に、セキュリティとパフォーマンスの観点から確認すべき項目があります。本記事では、Content Security Policy(CSP)、Subresource Integrity(SRI)、Preload、エラーレポートという 4 つの重要な要素について、実践的なチェックリストと実装方法を解説いたします。
これらの設定を適切に行うことで、XSS 攻撃を防ぎ、改ざんされたリソースの読み込みを防止し、初期表示速度を改善し、本番環境で発生するエラーを確実に捕捉できるようになります。
背景
本番環境で直面するセキュリティとパフォーマンスの課題
SolidJS は高速で効率的なフレームワークですが、本番環境では開発環境では気づかなかった問題が顕在化することがあります。
以下の図は、本番運用で考慮すべき 4 つの主要な領域を示しています。
mermaidflowchart TD
prod["本番環境<br/>SolidJSアプリ"]
prod --> csp["CSP<br/>スクリプト実行制御"]
prod --> sri["SRI<br/>リソース改ざん検証"]
prod --> preload["Preload<br/>リソース事前読込"]
prod --> error["エラーレポート<br/>障害検知・分析"]
csp --> csp_result["XSS攻撃防止"]
sri --> sri_result["CDN改ざん防止"]
preload --> preload_result["初期表示高速化"]
error --> error_result["障害早期発見"]
図の要点:本番運用の 4 つの柱
- CSP:悪意あるスクリプトの実行を防ぐ
- SRI:外部リソースの完全性を検証
- Preload:クリティカルパスを最適化
- エラーレポート:リアルタイムで問題を把握
セキュリティヘッダーの重要性
本番環境では、セキュリティヘッダーの設定が不十分だと、XSS(Cross-Site Scripting)攻撃やデータ漏洩のリスクが高まります。特に SolidJS のような JavaScript フレームワークでは、動的にコンテンツを生成するため、適切なセキュリティポリシーの設定が不可欠です。
パフォーマンス最適化の必要性
初期表示速度は、ユーザー体験と SEO の両方に大きな影響を与えます。SolidJS の小さなバンドルサイズという利点を最大限に活かすためには、リソースの読み込み戦略を最適化する必要があるのです。
課題
セキュリティ設定の複雑さ
本番環境で適切なセキュリティを確保するには、いくつかの課題があります。
# | 課題項目 | 具体的な問題 | 影響範囲 |
---|---|---|---|
1 | CSP 設定の難しさ | インラインスクリプトとの競合 | XSS 脆弱性 |
2 | SRI 導入の手間 | ハッシュ値の自動生成が必要 | 運用コスト増 |
3 | Preload 設定の判断 | どのリソースを優先すべきか不明 | 表示速度低下 |
4 | エラー監視の欠如 | 本番エラーが見えない | 障害対応遅延 |
以下の図は、これらの課題がどのように関連しているかを示しています。
mermaidflowchart LR
dev["開発環境"] --> deploy["デプロイ"]
deploy --> prod["本番環境"]
prod --> issue1["CSP未設定<br/>XSS脆弱性"]
prod --> issue2["SRI未適用<br/>改ざんリスク"]
prod --> issue3["Preload未設定<br/>表示遅延"]
prod --> issue4["エラー監視なし<br/>障害見逃し"]
issue1 --> risk["セキュリティ<br/>インシデント"]
issue2 --> risk
issue4 --> risk
issue3 --> ux["UX低下<br/>離脱率上昇"]
図の要点:課題の連鎖
設定不足がセキュリティインシデントや UX 低下を引き起こし、ビジネスに直接的な影響を与えます。
CSP と SolidJS の相性問題
SolidJS は効率的なランタイムを持ちますが、デフォルトの設定ではインラインスクリプトを使用する場合があります。厳格な CSP を設定すると、これらのスクリプトが実行されず、アプリケーションが正しく動作しない可能性があります。
CDN リソースの完全性検証
外部 CDN から読み込むライブラリやフォントが改ざんされていないことを保証する必要がありますが、SRI の設定は手動で行うと手間がかかり、ミスも起こりやすくなります。
エラーの可視化不足
開発環境ではconsole.error
で十分ですが、本番環境ではユーザーの環境で発生したエラーを捕捉し、開発チームにレポートする仕組みが必要です。エラーレポートがないと、障害が発生していても気づけないという事態になりかねません。
解決策
総合的な本番運用チェックリスト
これらの課題に対して、体系的なチェックリストを用意し、各項目について具体的な実装方法を提供します。
以下の表は、本番デプロイ前に確認すべき項目の一覧です。
# | カテゴリ | チェック項目 | 優先度 | 検証方法 |
---|---|---|---|---|
1 | CSP | Content-Security-Policy ヘッダー設定 | 高 | ブラウザ DevTools |
2 | CSP | nonce 値の動的生成 | 高 | レスポンスヘッダー確認 |
3 | SRI | 外部スクリプトの integrity 属性 | 高 | HTML 検証 |
4 | SRI | CSS ファイルの integrity 属性 | 中 | HTML 検証 |
5 | Preload | クリティカル JS の preload | 高 | Network 分析 |
6 | Preload | Web フォントの preload | 中 | Lighthouse |
7 | エラー | グローバルエラーハンドラー設定 | 高 | 意図的エラー発生 |
8 | エラー | 非同期エラーの捕捉 | 高 | Promise reject 確認 |
9 | エラー | エラー送信先の設定 | 高 | ログ確認 |
次のセクションでは、これらの項目を実装する具体的な方法を順に解説していきます。
解決アプローチの全体像
以下の図は、4 つの要素を実装する順序と依存関係を示しています。
mermaidstateDiagram-v2
[*] --> CSP設定
CSP設定 --> SRI実装
SRI実装 --> Preload最適化
Preload最適化 --> エラーレポート
エラーレポート --> 本番デプロイ
本番デプロイ --> 継続監視
継続監視 --> [*]
note right of CSP設定
セキュリティ基盤
XSS攻撃対策
end note
note right of SRI実装
リソース完全性
改ざん検知
end note
note right of Preload最適化
パフォーマンス
表示速度向上
end note
note right of エラーレポート
運用監視
障害早期発見
end note
段階的実装のポイント:
- まずセキュリティ基盤(CSP・SRI)を固める
- 次にパフォーマンス(Preload)を最適化
- 最後に運用監視(エラーレポート)を整備
具体例
1. Content Security Policy(CSP)の実装
CSP とは
Content Security Policy は、ブラウザに対して「どこからのリソース読み込みを許可するか」を指示するセキュリティ機構です。XSS 攻撃を防ぐ最も効果的な手段の一つとなっています。
Step 1: 基本的な CSP ヘッダーの設定
サーバー側で HTTP ヘッダーを設定します。Nginx の例を見ていきましょう。
nginx# nginx.conf または sites-available の設定ファイル
# 基本的なCSP設定
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-{NONCE_VALUE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
この設定により、以下の制限が適用されます。
default-src 'self'
:基本的に同じオリジンからのみ読み込みscript-src 'self' 'nonce-{NONCE_VALUE}'
:スクリプトは同じオリジンか nonce 付きのみstyle-src 'self' 'unsafe-inline'
:スタイルはインラインも許可(SolidJS 対応)connect-src 'self' https://api.example.com
:API 通信先を明示的に指定
Step 2: SolidJS アプリでの nonce 値の生成と埋め込み
SolidJS アプリケーションで nonce 値を動的に生成し、HTML に埋め込む実装です。
typescript// server/middleware/csp.ts
import { nanoid } from 'nanoid';
import type {
Request,
Response,
NextFunction,
} from 'express';
// CSP用のnonce値を生成するミドルウェア
export function generateNonce(
req: Request,
res: Response,
next: NextFunction
): void {
// ランダムなnonce値を生成(base64で32文字)
const nonce = nanoid(32);
// リクエストオブジェクトにnonce値を保存
res.locals.nonce = nonce;
next();
}
次に、生成した nonce 値を CSP ヘッダーと HTML の両方に適用します。
typescript// server/middleware/csp.ts(続き)
export function setCSPHeader(
req: Request,
res: Response,
next: NextFunction
): void {
const nonce = res.locals.nonce;
// CSPヘッダーにnonce値を埋め込む
const cspPolicy = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
`
.replace(/\s+/g, ' ')
.trim();
res.setHeader('Content-Security-Policy', cspPolicy);
next();
}
Step 3: HTML テンプレートへの nonce 適用
SolidJS のエントリーポイントとなる HTML ファイルに、nonce 値を設定します。
html<!-- index.html または server/template.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>SolidJS App</title>
<!-- nonce値を持つインラインスタイル(必要な場合) -->
<style nonce="{{NONCE}}">
/* クリティカルCSS */
body {
margin: 0;
font-family: sans-serif;
}
</style>
</head>
<body>
<div id="root"></div>
<!-- nonce値を持つスクリプトタグ -->
<script
nonce="{{NONCE}}"
type="module"
src="/src/index.tsx"
></script>
</body>
</html>
サーバー側でこのテンプレートをレンダリングする際、{{NONCE}}
を実際の値に置き換えます。
typescript// server/render.ts
import fs from 'fs';
import path from 'path';
export function renderHTML(nonce: string): string {
// HTMLテンプレートを読み込む
const template = fs.readFileSync(
path.resolve('./dist/index.html'),
'utf-8'
);
// nonce値を埋め込んでHTMLを返す
return template.replace(/\{\{NONCE\}\}/g, nonce);
}
CSP 設定の検証方法
ブラウザの開発者ツールで、CSP が正しく動作しているか確認できます。
javascript// ブラウザのコンソールで実行してテスト
// CSPに違反するスクリプトを実行しようとする
const script = document.createElement('script');
script.textContent = 'console.log("Unauthorized script")';
document.body.appendChild(script);
// CSPが正しく設定されていれば、以下のエラーが表示される
// Refused to execute inline script because it violates
// the following Content Security Policy directive
CSP の違反は、開発者ツールの Console タブで確認できます。正しく設定されている場合、不正なスクリプト実行は自動的にブロックされます。
2. Subresource Integrity(SRI)の実装
SRI とは
Subresource Integrity は、CDN などから読み込むリソースが改ざんされていないことを検証する仕組みです。リソースのハッシュ値をintegrity
属性に指定することで、ブラウザが自動的に検証を行います。
Step 1: 外部スクリプトへの SRI 適用
CDN から読み込むライブラリに SRI を設定します。
html<!-- 外部ライブラリの読み込み(SRIあり) -->
<!-- React(SolidJSと併用する場合) -->
<script
src="https://cdn.example.com/react@18.2.0/umd/react.production.min.js"
integrity="sha384-KyZXEAg3QhqLMpG8r+Knujsl5+5hb7IE6w4A5sI9P3C8a5e3sF9d0h8j7P3kL2mN"
crossorigin="anonymous"
></script>
<!-- Webフォント -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay"
crossorigin="anonymous"
/>
crossorigin="anonymous"
属性は、CORS リクエストを有効にし、SRI チェックを可能にします。
Step 2: ビルド時の自動 SRI 生成
Vite を使用している場合、プラグインで自動的に SRI ハッシュを生成できます。
typescript// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import { createHtmlPlugin } from 'vite-plugin-html';
import crypto from 'crypto';
import fs from 'fs';
// SRIハッシュを生成するヘルパー関数
function generateSRIHash(filePath: string): string {
const content = fs.readFileSync(filePath);
const hash = crypto
.createHash('sha384')
.update(content)
.digest('base64');
return `sha384-${hash}`;
}
export default defineConfig({
plugins: [
solidPlugin(),
createHtmlPlugin({
minify: true,
}),
],
build: {
rollupOptions: {
output: {
// ファイル名にハッシュを含める
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
});
ビルド後に SRI ハッシュを自動生成するスクリプトも作成しましょう。
typescript// scripts/generate-sri.ts
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { glob } from 'glob';
interface SRIMap {
[filename: string]: string;
}
// distディレクトリ内のすべてのJSとCSSファイルに対してSRIを生成
async function generateSRIForBuild(): Promise<void> {
const distDir = path.resolve('./dist');
const files = await glob('**/*.{js,css}', {
cwd: distDir,
});
const sriMap: SRIMap = {};
files.forEach((file) => {
const filePath = path.join(distDir, file);
const content = fs.readFileSync(filePath);
// SHA-384ハッシュを生成
const hash = crypto
.createHash('sha384')
.update(content)
.digest('base64');
sriMap[file] = `sha384-${hash}`;
console.log(`Generated SRI for ${file}`);
});
// SRIマップをJSONファイルに保存
fs.writeFileSync(
path.join(distDir, 'sri-map.json'),
JSON.stringify(sriMap, null, 2)
);
console.log('SRI generation complete!');
}
generateSRIForBuild().catch(console.error);
このスクリプトをpackage.json
のビルドプロセスに追加します。
json{
"scripts": {
"build": "vite build && tsx scripts/generate-sri.ts",
"dev": "vite",
"preview": "vite preview"
}
}
Step 3: SRI ハッシュを HTML に注入
生成された SRI ハッシュを HTML テンプレートに自動的に注入します。
typescript// server/inject-sri.ts
import fs from 'fs';
import path from 'path';
interface SRIMap {
[filename: string]: string;
}
export function injectSRIIntoHTML(
htmlPath: string
): string {
// SRIマップを読み込む
const sriMapPath = path.resolve('./dist/sri-map.json');
const sriMap: SRIMap = JSON.parse(
fs.readFileSync(sriMapPath, 'utf-8')
);
// HTMLファイルを読み込む
let html = fs.readFileSync(htmlPath, 'utf-8');
// script タグとlink タグを検索してSRIを追加
Object.entries(sriMap).forEach(([file, integrity]) => {
const fileName = path.basename(file);
// script タグにintegrity属性を追加
html = html.replace(
new RegExp(
`<script([^>]*src=["'][^"']*${fileName}["'][^>]*)>`,
'g'
),
`<script$1 integrity="${integrity}" crossorigin="anonymous">`
);
// link タグにintegrity属性を追加
html = html.replace(
new RegExp(
`<link([^>]*href=["'][^"']*${fileName}["'][^>]*)>`,
'g'
),
`<link$1 integrity="${integrity}" crossorigin="anonymous">`
);
});
return html;
}
SRI 検証のテスト
SRI が正しく機能しているか確認するため、意図的にハッシュ値を変更してテストします。
javascript// ブラウザコンソールでのテスト
// 正しいSRI
// <script src="/app.js" integrity="sha384-correct-hash"></script>
// → 読み込み成功
// 間違ったSRI
// <script src="/app.js" integrity="sha384-wrong-hash"></script>
// → Failed to find a valid digest in the 'integrity' attribute
3. Preload(リソース事前読込)の実装
Preload とは
Preload は、ブラウザに対して「このリソースを優先的に読み込んでほしい」と指示する仕組みです。クリティカルな JavaScript、CSS、フォントを事前に読み込むことで、初期表示を高速化できます。
以下の図は、Preload の効果を示しています。
mermaidsequenceDiagram
participant Browser
participant Server
participant CDN
Note over Browser,CDN: Preloadなしの場合
Browser->>Server: HTML要求
Server->>Browser: HTML返却
Browser->>Browser: HTMLパース開始
Browser->>Server: app.js要求
Server->>Browser: app.js返却
Browser->>CDN: font.woff2要求
CDN->>Browser: font.woff2返却
Note over Browser,CDN: Preloadありの場合
Browser->>Server: HTML要求
Server->>Browser: HTML返却(Preloadヒント含む)
Browser->>Server: app.js要求(即座)
Browser->>CDN: font.woff2要求(即座)
Server->>Browser: app.js返却
CDN->>Browser: font.woff2返却
Browser->>Browser: HTMLパース(すでにリソース取得済み)
Preload の効果:並列読み込みによる時間短縮
Preload を使うことで、HTML のパースを待たずにリソース取得を開始できます。
Step 1: クリティカル JavaScript の Preload
SolidJS アプリのメインバンドルを Preload で事前読み込みします。
html<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<!-- クリティカルJavaScriptのPreload -->
<link
rel="preload"
href="/assets/index.js"
as="script"
crossorigin="anonymous"
/>
<!-- クリティカルCSSのPreload -->
<link
rel="preload"
href="/assets/index.css"
as="style"
/>
<title>SolidJS App</title>
</head>
<body>
<div id="root"></div>
<!-- 実際のスクリプト読み込み -->
<script type="module" src="/assets/index.js"></script>
</body>
</html>
as
属性で、リソースの種類(script、style、font 等)を指定することで、ブラウザが適切な優先度で読み込みを行います。
Step 2: Web フォントの最適化 Preload
Web フォントは、テキスト表示に直接影響するため、優先的に Preload すべきです。
html<!-- Webフォントのpreload設定 -->
<head>
<!-- Google Fontsの場合:まずCSSをpreconnect -->
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
<!-- フォントファイル自体をpreload -->
<link
rel="preload"
href="https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- 自前のフォントファイルの場合 -->
<link
rel="preload"
href="/fonts/custom-font.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- 通常のCSSリンク -->
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
rel="stylesheet"
/>
</head>
フォントの Preload にはcrossorigin
属性が必須です。これを忘れると、フォントが二重に読み込まれる可能性があります。
Step 3: 動的 Preload の実装
SolidJS アプリ内で、次に必要になるリソースを動的に Preload する実装です。
typescript// src/utils/preload.ts
// リソースをプリロードするユーティリティ関数
export function preloadResource(
href: string,
as: 'script' | 'style' | 'image' | 'font' | 'fetch',
type?: string
): void {
// すでにpreloadされているか確認
const existing = document.querySelector(
`link[rel="preload"][href="${href}"]`
);
if (existing) {
return; // すでに存在する場合はスキップ
}
// link要素を作成
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = as;
if (type) {
link.type = type;
}
// フォントとfetchの場合はcrossorigin属性が必要
if (as === 'font' || as === 'fetch') {
link.crossOrigin = 'anonymous';
}
// headに追加
document.head.appendChild(link);
}
次に、ルート遷移時に必要なリソースを Preload します。
typescript// src/routes/Router.tsx
import { Router, Route } from '@solidjs/router';
import { lazy } from 'solid-js';
import { preloadResource } from '../utils/preload';
// コード分割されたコンポーネント
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
// ルート変更時にリソースをpreloadする関数
function preloadRouteResources(path: string): void {
switch (path) {
case '/dashboard':
// ダッシュボードで使用するAPIデータをpreload
preloadResource(
'https://api.example.com/dashboard/stats',
'fetch'
);
// ダッシュボード用のチャートライブラリをpreload
preloadResource('/assets/chart-library.js', 'script');
break;
case '/about':
// Aboutページで使用する画像をpreload
preloadResource('/images/team-photo.webp', 'image');
break;
}
}
export default function AppRouter() {
return (
<Router
onBeforeEnter={(to) => {
// ルート遷移前にリソースをpreload
preloadRouteResources(to.path);
}}
>
<Route path='/' component={Home} />
<Route path='/about' component={About} />
<Route path='/dashboard' component={Dashboard} />
</Router>
);
}
Step 4: Preload のパフォーマンス測定
Preload の効果を測定するため、Performance API を使用します。
typescript// src/utils/performance.ts
interface PreloadMetrics {
resourceName: string;
startTime: number;
duration: number;
wasPreloaded: boolean;
}
// Preloadされたリソースのパフォーマンスを測定
export function measurePreloadPerformance(): PreloadMetrics[] {
const resources =
performance.getEntriesByType('resource');
const preloadLinks = document.querySelectorAll(
'link[rel="preload"]'
);
const preloadedUrls = new Set(
Array.from(preloadLinks).map((link) =>
link.getAttribute('href')
)
);
return resources.map(
(resource: PerformanceResourceTiming) => ({
resourceName: resource.name,
startTime: resource.startTime,
duration: resource.duration,
wasPreloaded: preloadedUrls.has(
new URL(resource.name).pathname
),
})
);
}
この関数をブラウザのコンソールで実行すると、Preload の効果を数値で確認できます。
typescript// ブラウザコンソールでの実行例
const metrics = measurePreloadPerformance();
console.table(metrics.filter((m) => m.wasPreloaded));
// 出力例:
// | resourceName | duration | wasPreloaded |
// |------------------------|----------|--------------|
// | /assets/index.js | 45.3ms | true |
// | /fonts/custom.woff2 | 23.1ms | true |
4. エラーレポートの実装
エラーレポートの重要性
本番環境で発生するエラーを捕捉し、開発チームに通知する仕組みは必須です。ユーザーの環境でしか再現しないバグを見逃さないために、包括的なエラーレポートシステムを構築しましょう。
以下の図は、エラーレポートのフローを示しています。
mermaidflowchart TD
user_action["ユーザー操作"] --> app["SolidJSアプリ"]
app --> error_type{エラー種別}
error_type --> sync["同期エラー<br/>(実行時エラー)"]
error_type --> async["非同期エラー<br/>(Promise reject)"]
error_type --> resource["リソースエラー<br/>(404等)"]
sync --> error_handler["グローバル<br/>エラーハンドラー"]
async --> unhandled["unhandledrejection<br/>ハンドラー"]
resource --> resource_handler["Resource Error<br/>ハンドラー"]
error_handler --> collector["エラー情報<br/>収集"]
unhandled --> collector
resource_handler --> collector
collector --> send["エラーレポート<br/>送信"]
send --> backend["バックエンド<br/>ログ集約"]
backend --> alert["アラート通知<br/>(Slack等)"]
エラーレポートの流れ:包括的な捕捉と通知
Step 1: グローバルエラーハンドラーの設定
まず、すべてのエラーを捕捉するグローバルハンドラーを設定します。
typescript// src/error/global-handler.ts
interface ErrorReport {
message: string;
stack?: string;
url: string;
lineNumber?: number;
columnNumber?: number;
timestamp: number;
userAgent: string;
errorType: 'javascript' | 'promise' | 'resource';
}
// エラー情報を収集する関数
function collectErrorInfo(
error: Error | ErrorEvent | PromiseRejectionEvent,
errorType: ErrorReport['errorType']
): ErrorReport {
const baseInfo = {
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
errorType,
};
// Error オブジェクトの場合
if (error instanceof Error) {
return {
...baseInfo,
message: error.message,
stack: error.stack,
};
}
// ErrorEvent の場合(window.onerror)
if ('error' in error && error.error instanceof Error) {
return {
...baseInfo,
message: error.error.message,
stack: error.error.stack,
lineNumber: error.lineno,
columnNumber: error.colno,
};
}
// PromiseRejectionEvent の場合
if ('reason' in error) {
return {
...baseInfo,
message: String(error.reason),
stack: error.reason?.stack,
};
}
// その他の場合
return {
...baseInfo,
message: String(error),
};
}
次に、収集したエラー情報をバックエンドに送信する関数を実装します。
typescript// src/error/global-handler.ts(続き)
// エラーレポートを送信する関数
async function sendErrorReport(
report: ErrorReport
): Promise<void> {
try {
// エラーレポート用のエンドポイントに送信
await fetch('https://api.example.com/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(report),
// keepalive: ページ離脱時でも送信を完了
keepalive: true,
});
} catch (sendError) {
// エラー送信自体が失敗した場合はコンソールに記録
console.error(
'Failed to send error report:',
sendError
);
}
}
グローバルエラーハンドラーを設定します。
typescript// src/error/global-handler.ts(続き)
// グローバルエラーハンドラーを初期化
export function initializeErrorHandlers(): void {
// JavaScript実行時エラーを捕捉
window.addEventListener('error', (event: ErrorEvent) => {
const report = collectErrorInfo(event, 'javascript');
sendErrorReport(report);
// デフォルトの動作(コンソール出力)は継続
return false;
});
// Promise の unhandled rejection を捕捉
window.addEventListener(
'unhandledrejection',
(event: PromiseRejectionEvent) => {
const report = collectErrorInfo(event, 'promise');
sendErrorReport(report);
// デフォルトの動作を継続
return false;
}
);
// リソース読み込みエラーを捕捉(画像、スクリプト等)
window.addEventListener(
'error',
(event: Event) => {
const target = event.target as HTMLElement;
// リソース要素の場合のみ処理
if (
target instanceof HTMLImageElement ||
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement
) {
const report: ErrorReport = {
message: `Resource failed to load: ${
target instanceof HTMLImageElement
? target.src
: target instanceof HTMLScriptElement
? target.src
: (target as HTMLLinkElement).href
}`,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
errorType: 'resource',
};
sendErrorReport(report);
}
},
true // キャプチャフェーズで実行
);
}
Step 2: SolidJS ErrorBoundary の実装
SolidJS のコンポーネントツリー内でのエラーを捕捉するため、ErrorBoundary を実装します。
typescript// src/components/ErrorBoundary.tsx
import { ErrorBoundary as SolidErrorBoundary } from 'solid-js';
import type { ParentProps } from 'solid-js';
interface ErrorBoundaryProps extends ParentProps {
fallback?: (
error: Error,
reset: () => void
) => JSX.Element;
}
// エラー発生時に表示するデフォルトUI
function DefaultErrorFallback(
error: Error,
reset: () => void
): JSX.Element {
return (
<div class='error-boundary'>
<h2>エラーが発生しました</h2>
<p>
申し訳ございません。予期しないエラーが発生しました。
</p>
<details>
<summary>詳細情報</summary>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</details>
<button onClick={reset}>再試行</button>
</div>
);
}
export function ErrorBoundary(props: ErrorBoundaryProps) {
return (
<SolidErrorBoundary
fallback={(error: Error, reset: () => void) => {
// エラーをレポート
const report = {
message: error.message,
stack: error.stack,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
errorType: 'component' as const,
};
// バックエンドに送信
fetch('https://api.example.com/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
}).catch(console.error);
// カスタムfallbackまたはデフォルトを表示
return props.fallback
? props.fallback(error, reset)
: DefaultErrorFallback(error, reset);
}}
>
{props.children}
</SolidErrorBoundary>
);
}
アプリケーション全体を ErrorBoundary で囲みます。
typescript// src/App.tsx
import { ErrorBoundary } from './components/ErrorBoundary';
import { Router } from '@solidjs/router';
import { routes } from './routes';
export default function App() {
return (
<ErrorBoundary>
<Router>{routes}</Router>
</ErrorBoundary>
);
}
Step 3: 非同期処理のエラーハンドリング
SolidJS のリソース(データフェッチング)でのエラーを適切に処理します。
typescript// src/api/fetch-data.ts
import { createResource } from 'solid-js';
interface User {
id: number;
name: string;
email: string;
}
// ユーザーデータを取得する関数
async function fetchUser(userId: number): Promise<User> {
const response = await fetch(
`https://api.example.com/users/${userId}`
);
if (!response.ok) {
// HTTPエラーを明示的にスロー
throw new Error(
`HTTP Error ${response.status}: ${response.statusText}`
);
}
return response.json();
}
// SolidJSのResourceとして使用
export function createUserResource(userId: () => number) {
const [user, { mutate, refetch }] = createResource(
userId,
fetchUser,
{
// エラー時の動作を設定
onError: (error: Error) => {
// エラーをレポート
const report = {
message: `API Error: ${error.message}`,
stack: error.stack,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
errorType: 'api' as const,
};
fetch('https://api.example.com/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
}).catch(console.error);
},
}
);
return { user, mutate, refetch };
}
コンポーネント側でエラー状態を処理します。
typescript// src/components/UserProfile.tsx
import { Show, Suspense } from 'solid-js';
import { createUserResource } from '../api/fetch-data';
interface UserProfileProps {
userId: number;
}
export function UserProfile(props: UserProfileProps) {
const { user, refetch } = createUserResource(
() => props.userId
);
return (
<Suspense fallback={<div>読み込み中...</div>}>
<Show
when={!user.error}
fallback={
<div class='error-message'>
<p>ユーザー情報の取得に失敗しました</p>
<button onClick={() => refetch()}>
再試行
</button>
</div>
}
>
<div class='user-profile'>
<h2>{user()?.name}</h2>
<p>{user()?.email}</p>
</div>
</Show>
</Suspense>
);
}
Step 4: エラーレポートの初期化
アプリケーションのエントリーポイントでエラーハンドラーを初期化します。
typescript// src/index.tsx
import { render } from 'solid-js/web';
import App from './App';
import { initializeErrorHandlers } from './error/global-handler';
// グローバルエラーハンドラーを初期化
initializeErrorHandlers();
// アプリケーションをマウント
const root = document.getElementById('root');
if (!root) {
throw new Error('Root element not found');
}
render(() => <App />, root);
エラーレポートの確認
本番環境でエラーが正しくレポートされているか、意図的にエラーを発生させてテストします。
typescript// 開発環境でのテスト用コード(本番では削除)
// テスト1: 同期エラー
function testSyncError() {
throw new Error('Test sync error');
}
// テスト2: 非同期エラー
function testAsyncError() {
return Promise.reject(new Error('Test async error'));
}
// テスト3: リソースエラー
function testResourceError() {
const img = document.createElement('img');
img.src = 'https://example.com/nonexistent.jpg';
document.body.appendChild(img);
}
// ブラウザコンソールで実行
// testSyncError();
// testAsyncError();
// testResourceError();
これらのテストを実行すると、バックエンドのエラーログエンドポイントにエラー情報が送信されることを確認できます。
本番運用チェックリストの最終確認
すべての実装が完了したら、以下のチェックリストで最終確認を行いましょう。
# | 項目 | 確認内容 | 確認方法 | ステータス |
---|---|---|---|---|
1 | CSP ヘッダー | Content-Security-Policy が設定されている | DevTools Network → Headers | ☐ |
2 | CSP nonce | スクリプトに nonce 属性が付与されている | HTML ソース確認 | ☐ |
3 | CSP 違反検知 | 不正なスクリプト実行がブロックされる | Console で CSP violation 確認 | ☐ |
4 | SRI 外部スクリプト | CDN スクリプトに integrity 属性あり | HTML ソース確認 | ☐ |
5 | SRI 自動生成 | ビルド時に SRI ハッシュが生成される | yarn build 実行確認 | ☐ |
6 | Preload JS | クリティカル JS に preload 設定 | HTML ソース確認 | ☐ |
7 | Preload Font | Web フォントに preload 設定 | HTML ソース確認 | ☐ |
8 | Preload 効果 | Performance API で preload 確認 | measurePreloadPerformance() 実行 | ☐ |
9 | エラーハンドラー | グローバルエラーハンドラー初期化済み | window.onerror 確認 | ☐ |
10 | ErrorBoundary | コンポーネントエラーを捕捉 | 意図的エラーでテスト | ☐ |
11 | API エラー | API エラーがレポートされる | 存在しない API 呼び出し | ☐ |
12 | エラー送信 | エラーがバックエンドに届く | ログ確認 | ☐ |
すべての項目にチェックが入れば、本番デプロイの準備が整ったことになります。
まとめ
本記事では、SolidJS アプリケーションを本番環境で安全かつ高速に運用するための 4 つの重要な要素について解説しました。
実装した 4 つの柱
-
Content Security Policy(CSP):XSS 攻撃を防ぐためのセキュリティポリシーを設定し、nonce 値を使ってインラインスクリプトを安全に実行できるようにしました。
-
Subresource Integrity(SRI):CDN などの外部リソースが改ざんされていないことを検証する仕組みを実装し、ビルド時に自動的にハッシュ値を生成する方法を紹介しました。
-
Preload(リソース事前読込):クリティカルな JavaScript、CSS、Web フォントを優先的に読み込むことで、初期表示速度を改善する手法を実装しました。
-
エラーレポート:本番環境で発生するすべてのエラーを捕捉し、バックエンドに送信する包括的なエラーハンドリングシステムを構築しました。
本番運用で得られるメリット
これらの実装により、以下のメリットが得られます。
メリット | 具体的な効果 |
---|---|
セキュリティ向上 | XSS 攻撃やリソース改ざんのリスクを大幅に低減 |
パフォーマンス改善 | 初期表示速度が向上し、Core Web Vitals スコアが改善 |
運用安定性 | エラーを早期に発見し、迅速な対応が可能になる |
ユーザー体験向上 | 高速で安全なアプリケーションにより、満足度が向上 |
SEO 効果 | ページ速度の改善が SEO ランキングに好影響 |
次のステップ
本記事で紹介したチェックリストを活用して、段階的に実装を進めていきましょう。まずは CSP と SRI でセキュリティ基盤を固め、次に Preload でパフォーマンスを最適化し、最後にエラーレポートで運用体制を整えるという順序がおすすめです。
すべての実装が完了したら、実際に本番環境でテストを行い、各項目が正しく機能していることを確認してください。継続的な監視と改善により、より安全で高速な SolidJS アプリケーションを維持できるようになります。
関連リンク
公式ドキュメント
- SolidJS 公式サイト
- Content Security Policy (CSP) - MDN
- Subresource Integrity - MDN
- Resource Hints: preload - MDN
セキュリティ関連
パフォーマンス関連
エラー監視サービス
- article
SolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検
- article
SolidJS クリーンアーキテクチャ実践:UI・状態・副作用を厳密に分離する
- article
SolidJS フック相当 API 速見表:createSignal/createMemo/createEffect… 一覧
- article
SolidJS を macOS + yarn で最速構築:ESLint・Prettier・TSconfig の鉄板レシピ
- article
SolidJS × TanStack Query vs createResource:データ取得手段の実測比較
- article
SolidJS の hydration mismatch を根絶する:原因パターン 12 と再発防止チェック
- article
ESLint 運用ダッシュボード:SARIF/Code Scanning で違反推移を可視化
- article
SolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検
- article
Redis 使い方:Next.js で Cache-Tag と再検証を実装(Edge/Node 両対応)
- article
Dify 本番運用ガイド:SLO/SLA 設定とアラート設計のベストプラクティス
- article
Cursor の KPI 設計:リードタイム・欠陥率・レビュー時間を定量で追う
- article
Python 本番運用 SLO 設計:p95 レイテンシ・エラー率・スループットの指標化
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来