「Node.js 製のサイトで、なぜか Microsoft Edge のときだけ CSS が読み込まれない」。Chrome や Firefox では正常なのに、Edge だけ崩れる案件は現場で意外と頻出します。本記事では原因の切り分け方を網羅し、MIME/CORS/Service Worker/混在コンテンツ/圧縮設定など、再現性の高いチェックポイントと具体的な修正例(Express/Nginx/Apache/ビルドツール)を一気通貫で提示します。
前提整理 ― 「nord.js」は誤記で Node.js のこと
質問原文では “nord.js” とありますが、文脈・構成から Node.js を指していると解釈します。サーバーは Express/Koa/Next.js/Vite 等いずれでも同様に読み進められるよう、HTTP レイヤの原則とブラウザ(Edge)側の挙動に寄せて解説します。
最短ルートの再現確認:Edge Insider(Dev/Beta/Canary)
まず Edge Insider 系列(Dev 版など)に同じ URL を読み込ませ、安定版のみで発生するのかを確認します。Insider 版で再現しないなら、既知不具合が次期安定版で解消予定の可能性が高いです。検証のうえ、暫定的に Insider 版を案内するか、安定版の修正版公開まで回避策で凌ぎます。
一目で分かる:症状別の第一手
| 見える症状 | 開発者ツールの所見 | 第一に疑う点 | すぐ試す対処 |
|---|---|---|---|
| CSS だけ 404/ERR_ABORTED | Network で 404 または ERR_ABORTED | パス/ビルド成果物の配置ズレ | 出力ディレクトリと <link href> を突き合わせ、大小文字や拡張子違いを修正 |
| Console に「MIME type mismatch」 | Response Headers が text/html 等 | Content-Type 誤設定 + X-Content-Type-Options: nosniff | CSS へ text/css; charset=utf-8 を厳格送出、サーバまたはリバースプロキシを修正 |
| Edge のみブロック | Mixed Content / Tracking prevention | HTTPS/HTTP 混在、CDN ドメインのブロック | 全リソースを https 化。Edge の追跡防止が誤判定する URL ならホスト名を変更 |
| 更新しても崩れたまま | 200 だが古い ETag/Cache-Control | SW/ブラウザ/中間キャッシュのスタック | ハードリロード、Service Worker の停止・再登録、キャッシュバスティングを導入 |
| ネットワークは 200 だが描画反映されない | link rel="preload" からの二重取得や as 不一致 | preload 設定不備、CSP による棄却 | as="style" と crossorigin を整備、CSP の style-src を調整 |
| Edge でのみ「Decoding failed」 | ERR_CONTENT_DECODING_FAILED | 二重 gzip/brotli、誤った Content-Encoding | 圧縮の多重適用を止め、Vary: Accept-Encoding を設定 |
| 一部ページだけ崩れる | SPAs の一部ルートで 404 → HTML フォールバック | サーバ側のヒストリーフォールバックが CSS にも適用 | 静的配信の優先順位を CSS に、HTML は最後にルーティング |
基本のトラブルシューティング
キャッシュクリアと強制再読み込み
- Edge 設定でキャッシュを削除し、
Ctrl + F5(ハードリロード)。 - 同じユーザープロファイルでも、拡張・追跡防止の影響を排除するためInPrivateで再現確認。
Network タブの確認ポイント
- CSS リクエストの Status(200/304/404/204/206/499 など)と Type(
stylesheetか)を確認。 - Response Headers の
Content-Type、Content-Encoding、Cache-Control、ETag、Varyを確認。 - Console に
MIME type ("text/html") is not a supported stylesheet MIME type、Blocked:mixed-content、ERR_CONTENT_DECODING_FAILED、net::ERR_BLOCKED_BY_CLIENT等がないか。
よくある原因と即効処方
- MIME 型の誤り:CSS に
text/htmlが返っている。
→ サーバ/プロキシの types 設定やdefault_typeを修正し、text/cssを厳格送出。X-Content-Type-Options: nosniffを付けるなら特に厳密に。 - HTTPS/HTTP の混在:HTML は https、CSS は http。
→ すべて https に統一。CSP がblock-all-mixed-contentの場合は必ず混在排除。 - CORS の欠落:外部ドメインの CSS を取得。
→ 応答にAccess-Control-Allow-Originを付与(CDN 設定も忘れず)。preload するならcrossoriginも整合。 - Service Worker のスタック:旧 CSS をオフラインキャッシュから返却。
→ Application タブで SW を一旦無効化・登録解除。キャッシュキーにビルドハッシュを採用。 - 圧縮不整合:二重 gzip/brotli、または
Content-Encodingとボディが不一致。
→ どこで圧縮するか 一箇所 に決める。Vary: Accept-Encodingを必ず送る。 - ファイル先頭の BOM/制御文字:CSS パーサがこける。
→ エディタで「UTF-8(BOM なし)」保存。ビルドパイプラインでも BOM を生成しない。 - preload の誤用:
as不一致やcrossorigin未指定。
→<link rel="preload" href="/app.css" as="style" crossorigin>+ onload でrelをstylesheetに切替。 - CSP(Content-Security-Policy):
style-src制約で棄却。
→ 外部ホストを許可、または nonce/ハッシュを採用(unsafe-inlineは暫定扱い)。 - Edge の追跡防止/拡張:一部 CDN がブロックリスト該当。
→ InPrivate/拡張無効で再現しないなら、CDN ホスト名変更や自前配信を検討。
HTTP ヘッダーの「正解例」と「NG 例」
| 項目 | 正解例(OK) | NG 例 | 理由 |
|---|---|---|---|
| Content-Type | text/css; charset=utf-8 | text/html / application/octet-stream | nosniff が有効だと非 CSS は読み込み拒否 |
| Content-Encoding | gzip or br(ボディと一致) | ヘッダーは br だが実体は未圧縮 | Edge では ERR_CONTENT_DECODING_FAILED を誘発 |
| Cache-Control | public, max-age=31536000, immutable(ハッシュ名) | no-cache 乱用 | キャッシュ戦略が破綻すると SW や 304 の罠にハマる |
| Vary | Accept-Encoding | なし | 中間キャッシュが圧縮不一致を配る原因に |
| CSP | style-src 'self' cdn.example.com | style-src 'none' のまま | 正規の外部 CSS も棄却される |
Node.js(Express)の堅牢な配信テンプレート
import express from "express";
import path from "path";
import compression from "compression";
import helmet from "helmet";
import cors from "cors";
const app = express();
// 必要に応じて CSP を調整(inline/hashes/nonce)
app.use(helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // 本番は nonce/hashes を推奨
}
},
crossOriginEmbedderPolicy: false
}));
// 外部配信がある場合のみ調整
app.use(cors({
origin: [/^https?://localhost(:\d+)?$/],
methods: ["GET", "HEAD", "OPTIONS"]
}));
// 圧縮はサーバのどこか1箇所だけ
app.use(compression());
// 静的ファイルの厳格配信(CSS の Content-Type/キャッシュ/ETag)
app.use("/assets", express.static(path.join(process.cwd(), "public"), {
fallthrough: false,
etag: true,
immutable: true,
maxAge: "30d",
setHeaders(res, filePath) {
if (filePath.endsWith(".css")) {
res.setHeader("Content-Type", "text/css; charset=utf-8");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Vary", "Accept-Encoding");
}
}
}));
app.get("/", (_req, res) => {
res.sendFile(path.join(process.cwd(), "public/index.html"));
});
app.listen(3000, () => {
console.log("Server on [http://127.0.0.1:3000](http://127.0.0.1:3000)");
});
ポイント:
express.staticは拡張子から Content-Type を付与しますが、リバースプロキシで上書きされると崩れます。最終的に「Edge へ届ける応答ヘッダー」を確認しましょう。- 圧縮は二重にかけない(Node と Nginx の どちらか一方)。
- ハッシュ名(
app.7ce1f9.css)を採用し、immutableを付与すると更新反映が安定します。
Nginx/Apache/CDN の設定でハマりがちな罠
Nginx の基礎設定
# MIME の定義
types {
text/css css;
# ほかの必要な types...
}
# default_type を text/html にしない(誤爆の温床)
# default_type application/octet-stream; などにするか、types で厳密に
# 圧縮(どちらか、またはCDNへ委譲)
gzip on;
gzip_types text/css application/javascript;
# brotli モジュール使用時
brotli on;
brotli_types text/css application/javascript;
location /assets/ {
root /var/www/site/public;
add_header X-Content-Type-Options nosniff;
add_header Vary "Accept-Encoding";
expires 30d;
etag on;
}
Apache(httpd)の基礎設定
<IfModule mime_module>
AddType text/css .css
</IfModule>
# 圧縮
AddOutputFilterByType DEFLATE text/css application/javascript
# キャッシュ
Header set X-Content-Type-Options "nosniff"
Header set Cache-Control "public, max-age=2592000, immutable"
Header append Vary "Accept-Encoding"
「Edge だけ」を生むブラウザ挙動の差
- 追跡防止(Tracking prevention):特定の CDN/パスがトラッカー扱いになることがあります。InPrivate や追跡防止を「基本」に落として再現が消えるなら、配信ホストの変更や URL パターンの見直しを。
- MIME 厳格性:
nosniff下でtext/htmlを返すと CSS としては読みません。Chrome でも同様ですが、Edge の環境差(例えば拡張や企業ポリシー)で Edge のみ顕在化しがちです。 - preload の整合性:
as="style"と実際のContent-Typeが一致しない、crossoriginが足りない等で読み込みを棄却する場合があります。
Service Worker(Workbox/PWA)による取り違え
古いキャッシュを返し続ける、ヘッダーが変質する、404 をキャッシュしてしまう等で「Edge でだけ崩れる」ことがあります。以下のチェックを行います。
- Application → Service Workers で Unregister、ページをリロードして再現が消えるか。
- Workbox のルーティングで CSS を NetworkFirst に、キャッシュキーにファイルハッシュを含める。
self.skipWaiting()とclients.claim()の採用と、バージョニング(v1::などのプレフィックス)で確実に更新。
文字コード/BOM/制御文字
CSS 冒頭の BOM(EF BB BF)や不可視制御文字でパースが失敗する例があります。ビルド後の CSS を 16 進ダンプで確認し、先頭に EF BB BF があれば除去します。エディタ設定を「UTF-8(BOM なし)」に統一し、CI で Lint することを推奨します。
preload / rel=stylesheet の最適パターン
<link rel="preload" href="/assets/app.css" as="style" crossorigin
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/app.css"></noscript>
as="style"を必ず指定。- クロスオリジンなら
crossoriginを揃える。CORS と整合を取る。 - preload だけでは適用されないため
onloadでrelを切り替えるか、最初からrel="stylesheet"を使う。
混在コンテンツ(HTTPS/HTTP)を確実に潰す
- HTML・CSS・フォント・画像・JS すべてを https 化。
- CSP で
upgrade-insecure-requestsを付ける(ただしレガシー資産が多い場合は段階的に)。 - HSTS を導入して http へのダウングレードを抑止。
CORS を必要最小限で通す
import cors from "cors";
app.use("/assets", cors({
origin: ["https://app.example.com"], // 必要な発行元だけ
methods: ["GET", "HEAD"],
maxAge: 86400
}));
CDN 側にも Access-Control-Allow-Origin の設定が必要です(特にフォントや CSS を外部配信する場合)。
ビルドツール(Vite/webpack/Next.js)での落とし穴
- 成果物の置き場所:本番サーバの静的ルート(例:
/var/www/site/public)に/assets/app.[hash].cssが出ているか。 - HTML テンプレートの参照先:古い
href="/assets/app.oldhash.css"を指していないか。 - SSR/SPA のフォールバック:ルーター設定で CSS パスまで HTML を返していないか。
Node.js 側で「今の応答」を機械的に検査する
# CSS ヘッダーの現物を確認
curl -I http://127.0.0.1:3000/assets/app.css
# 圧縮の整合
curl -H "Accept-Encoding: br" -I [http://127.0.0.1:3000/assets/app.css](http://127.0.0.1:3000/assets/app.css)
curl -H "Accept-Encoding: gzip" -I [http://127.0.0.1:3000/assets/app.css](http://127.0.0.1:3000/assets/app.css)
ここで Content-Type: text/css と Content-Encoding/Vary の整合が取れていなければ、まずサーバ/プロキシ設定を見直します。
エラー別の深掘り対処集
| Console/Network の表示 | 主因 | 修正 |
|---|---|---|
MIME type ("text/html") ... | HTML を返している/拡張子ルーティングの衝突 | 静的配信を CSS 優先、SPA の HTML フォールバックは 最後 に適用 |
ERR_CONTENT_DECODING_FAILED | 二重圧縮 or ヘッダーと実体の不一致 | 圧縮点を一箇所に。CDN/リバプロ/アプリのどれが圧縮するか決める |
Blocked:mixed-content | http 経由の CSS | https 化。CSP で upgrade-insecure-requests |
net::ERR_BLOCKED_BY_CLIENT | 拡張/追跡防止によるブロック | InPrivate で再現確認。CDN ホスト名の変更も検討 |
Preloaded resource ... was not used | as 不一致、CORS 不整合 | as="style" の整合、crossorigin 指定、CORS 応答調整 |
ミニマム再現コード(MRE)の作り方と報告
- 最小の HTML + 1 枚の CSS(100 行以内)を用意。
- Express の静的配信のみで構成し、Nginx/CDN を経由せずに再現させる。
- それで再現しなければ、中間層(Nginx/CDN/SW)に問題があると確定。
- Edge 付属のフィードバック機能から再現手順、Edge バージョン、
edge://version情報、Network HAR を添付して報告。
実運用の再発防止チェックリスト
- CI での静的検査:ビルド後 CSS を Lint(BOM/制御文字検査)。
- E2E テスト:Playwright で Edge チャンネルを使い、主要画面のスタイル適用を検証。
- キャッシュキーの規律:ファイル名ハッシュ +
immutable、HTML 側はno-storeで最新を取得。 - ヘッダーのスナップショット:本番前に
curl -Iを CI で実行、Content-Typeなどを自動断言。
サンプル:Playwright で「CSS が効いている」を自動確認
import { test, expect, chromium } from "@playwright/test";
test("Edge で CSS が適用される", async () => {
const browser = await chromium.launch();
const context = await browser.newContext({
// 実機 Edge チャンネルを指定可能(環境依存)
channel: "msedge",
});
const page = await context.newPage();
await page.goto("[http://127.0.0.1:3000](http://127.0.0.1:3000)");
// 代表的な要素の計算スタイルを検査
const color = await page.evaluate(() => {
const el = document.querySelector("h1");
return el ? getComputedStyle(el).color : "";
});
expect(color).not.toBe(""); // 空=CSS 未適用の疑い
await browser.close();
});
ケーススタディで理解を固める
ケースA:Edge だけ「読み込みは 200 だが見た目が崩れる」
原因:link rel="preload" を使っているが as が style でなく、crossorigin も未指定。
解決:as="style" に修正+CORS 整合、または単純に rel="stylesheet" に戻す。
ケースB:Edge だけ「MIME mismatch」
原因:Nginx の default_type text/html が静的配信へ漏れ、CSS へ HTML が付与。
解決:types と default_type を見直し、text/css を正しく送る。nosniff は維持可。
ケースC:Edge だけ「decoding failed」
原因:アプリで gzip 済みのファイルを Nginx でも brotli 圧縮、二重圧縮に。
解決:圧縮は CDN または Nginx のみにし、アプリ側の圧縮を止める。Vary も忘れず。
実装コピペ OK:HTML の安全な読み込み例
<!-- preconnect で CSS/フォントを高速化(CORS 整合要) -->
<link rel="preconnect" href="https://static.example.com" crossorigin>
まとめ
Edge だけ CSS が読み込まれない場合でも、Network で事実を確認→HTTP ヘッダーの整合→中間層(SW/CDN/プロキシ)の排除→ビルド成果物の参照確認の順で進めれば、ほぼ必ず原因に到達します。Insider 版で再現しない場合はブラウザ側の既知不具合も疑い、最小再現を用意して報告すれば、修正版を待つ間の回避策も明確になります。ここまでの手順をテンプレ化しておけば、チーム全体で同種の不具合を短時間で収束できます。
付録:チェックリスト(コピペ用)
- Edge Insider(Dev/Beta)で再現可/不可
- Network:
Status、Type=stylesheet、Content-Type=text/css、Content-Encoding、Vary - Console:MIME mismatch/mixed content/decoding failed/blocked by client
- HTTPS 統一、CSP と CORS の整合
- Service Worker:無効化で改善するか、キャッシュキーはビルドハッシュか
- 圧縮:二重圧縮になっていないか、圧縮点は一箇所か
- ビルド成果物:ファイル名ハッシュ、HTML の参照先、SPA ルーティングとの衝突なし
- BOM/制御文字なし
最後に:質問への「即答テンプレ」
- Edge Insider で再現するか? → しないなら修正版待ち(暫定回避)。
- Network と Console のログ をとり、MIME・CORS・圧縮・混在・SW のどれかに当たりをつける。
- 最小再現を作成し、サーバ単体/プロキシ経由で差分比較。必要ならフィードバックから報告。

コメント