Microsoft EdgeでCSSが読み込まれない原因と対処法|Node.js/Express・CORS・MIME・Service Workerまで完全解説

「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_ABORTEDNetwork で 404 または ERR_ABORTEDパス/ビルド成果物の配置ズレ出力ディレクトリと <link href> を突き合わせ、大小文字や拡張子違いを修正
Console に「MIME type mismatch」Response Headers が text/html 等Content-Type 誤設定 + X-Content-Type-Options: nosniffCSS へ text/css; charset=utf-8 を厳格送出、サーバまたはリバースプロキシを修正
Edge のみブロックMixed Content / Tracking preventionHTTPS/HTTP 混在、CDN ドメインのブロック全リソースを https 化。Edge の追跡防止が誤判定する URL ならホスト名を変更
更新しても崩れたまま200 だが古い ETag/Cache-ControlSW/ブラウザ/中間キャッシュのスタックハードリロード、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 等がないか。

よくある原因と即効処方

  1. MIME 型の誤り:CSS に text/html が返っている。
    → サーバ/プロキシの types 設定や default_type を修正し、text/css を厳格送出。X-Content-Type-Options: nosniff を付けるなら特に厳密に。
  2. HTTPS/HTTP の混在:HTML は https、CSS は http。
    → すべて https に統一。CSP が block-all-mixed-content の場合は必ず混在排除。
  3. CORS の欠落:外部ドメインの CSS を取得。
    → 応答に Access-Control-Allow-Origin を付与(CDN 設定も忘れず)。preload するなら crossorigin も整合。
  4. Service Worker のスタック:旧 CSS をオフラインキャッシュから返却。
    → Application タブで SW を一旦無効化・登録解除。キャッシュキーにビルドハッシュを採用。
  5. 圧縮不整合:二重 gzip/brotli、または Content-Encoding とボディが不一致。
    → どこで圧縮するか 一箇所 に決める。Vary: Accept-Encoding を必ず送る。
  6. ファイル先頭の BOM/制御文字:CSS パーサがこける。
    → エディタで「UTF-8(BOM なし)」保存。ビルドパイプラインでも BOM を生成しない。
  7. preload の誤用:as 不一致や crossorigin 未指定。
    → <link rel="preload" href="/app.css" as="style" crossorigin> + onload で rel を stylesheet に切替。
  8. CSP(Content-Security-Policy):style-src 制約で棄却。
    → 外部ホストを許可、または nonce/ハッシュを採用(unsafe-inline は暫定扱い)。
  9. Edge の追跡防止/拡張:一部 CDN がブロックリスト該当。
    → InPrivate/拡張無効で再現しないなら、CDN ホスト名変更や自前配信を検討。

HTTP ヘッダーの「正解例」と「NG 例」

項目正解例(OK)NG 例理由
Content-Typetext/css; charset=utf-8text/html / application/octet-streamnosniff が有効だと非 CSS は読み込み拒否
Content-Encodinggzip or br(ボディと一致)ヘッダーは br だが実体は未圧縮Edge では ERR_CONTENT_DECODING_FAILED を誘発
Cache-Controlpublic, max-age=31536000, immutable(ハッシュ名)no-cache 乱用キャッシュ戦略が破綻すると SW や 304 の罠にハマる
VaryAccept-Encodingなし中間キャッシュが圧縮不一致を配る原因に
CSPstyle-src 'self' cdn.example.comstyle-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 の最適パターン

&lt;link rel="preload" href="/assets/app.css" as="style" crossorigin
      onload="this.onload=null;this.rel='stylesheet'"&gt;
&lt;noscript&gt;&lt;link rel="stylesheet" href="/assets/app.css"&gt;&lt;/noscript&gt;
  • 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-contenthttp 経由の CSShttps 化。CSP で upgrade-insecure-requests
net::ERR_BLOCKED_BY_CLIENT拡張/追跡防止によるブロックInPrivate で再現確認。CDN ホスト名の変更も検討
Preloaded resource ... was not usedas 不一致、CORS 不整合as="style" の整合、crossorigin 指定、CORS 応答調整

ミニマム再現コード(MRE)の作り方と報告

  1. 最小の HTML + 1 枚の CSS(100 行以内)を用意。
  2. Express の静的配信のみで構成し、Nginx/CDN を経由せずに再現させる。
  3. それで再現しなければ、中間層(Nginx/CDN/SW)に問題があると確定。
  4. 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/制御文字なし

最後に:質問への「即答テンプレ」

  1. Edge Insider で再現するか? → しないなら修正版待ち(暫定回避)。
  2. Network と Console のログ をとり、MIME・CORS・圧縮・混在・SW のどれかに当たりをつける。
  3. 最小再現を作成し、サーバ単体/プロキシ経由で差分比較。必要ならフィードバックから報告。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次