`:visited`の深層:プライバシーの壁とレンダリング最適化の狭間でCSSアーキテクトが取るべき戦略
フロントエンドのパフォーマンスチューニングやアクセシビリティの監査において、私たちは日々DOMの肥大化、不要な再描画(Reflow/Repaint)、そしてコンポーネントのライフサイクル管理に頭を悩ませている。
だが、一度立ち止まって考えてみてほしい。
モダンWebアプリケーションの要塞とも言えるSPA(Single Page Application)や、厳格に設計されたデザインシステムにおいて、リンクの「既読状態」を司る `:visited` 疑似クラスが、どれほど異質で、かつブラウザの内部メカニズムの深部に突き刺さる仕様であるかを意識したことがあるだろうか。
今回は、この `:visited` に焦点を当て、ブラウザのセキュリティサンドボックス、メモリ効率、そしてレンダリングエンジン(Blink, WebKit, Gecko)の内部挙動に至るまで、シニアエンジニアが知るべきすべての知見を解き明かしていく。
—
1. なぜ `:visited` は「制限された幽霊」なのか?
CSSの他の疑似クラス(`:hover`, `:active`, `:focus` 等)が、要素のインタラクションやフォーカス状態という「今ここにある文脈」をダイナミックに表現するのに対し、`:visited` はユーザーの「過去の履歴」という時系列データを扱う。
ここでまず思い出すべきなのが、2010年に発覚した深刻なプライバシー脆弱性(CSS History Stealing)だ。悪意あるサイトが `:visited` に適用されたスタイルを `getComputedStyle` やリンクのレイアウト情報(サイズや位置)を通じて読み取り、ユーザーが特定のサイト(銀行、医療機関、SNSなど)にアクセスした履歴を網羅的に窃取するという攻撃が猛威を振るった。
この歴史的背景から、現在のブラウザエンジンは `:visited` に対して厳格なスタイルのホワイトリスト制限(Sanitization)を課している。
適用できるプロパティの厳格な制限
ブラウザの内部では、`:visited` に適用できるプロパティがハードコードされている。大まかに言えば、以下のプロパティのみが上書きを許されている。
- `color`
- `background-color` (※ただし、不透明度の計算に制限がかかる場合がある)
- `border-color` (各辺)
- `outline-color`
- `column-rule-color`
- SVGの `fill` および `stroke`
逆に言えば、`font-size`, `width`, `height`, `display`, `visibility`, `transform` などのレイアウトやジオメトリ(幾何学)に影響を与えるプロパティは、すべて無視される。これにより、要素のサイズ変化を通じたサイドチャネル攻撃(Timing Attack / Rendering Attack)を根本から断っているのだ。
—
2. ブラウザ内部における `:visited` のメモリ効率とデータ構造
ギークな視点から最も興味深いのは、ブラウザがどのようにして「既読」を判定しているかというメモリ上の挙動だ。
ブラウザの履歴データベース(History Database)は、セキュリティ上の理由から、レンダリングエンジン(Blink等)から直接高速にクエリを投げられる構造になっていない。もし描画のたびにメインスレッドがストレージや暗号化された履歴DBにアクセスしていれば、無限スクロールや大量のリンクを持つECサイトのリストページは瞬時にフレームレートが崩壊する。
そのため、ブラウザは各ドキュメント(Document)のライフサイクル内において、Visited Link Hash Table(既読リンクハッシュテーブル)というインメモリのキャッシュ構造を保持している。
レンダリング負荷とセキュリティのトレードオフ
リンクがDOMツリーにマウントされる際、ブラウザはその `href` のURLをハッシュ化し、インメモリのハッシュテーブルと照合する。この照合処理は極めて高速に行われるが、SPA環境下において致命的な問題を引き起こす。
SPAでは、ページ遷移(ルーティング)が発生しても実際のブラウザの履歴(History API)は非同期に更新されるが、DOMが動的に書き換えられた際の `:visited` の再評価コストはバカにならない。数千件のアイテムを持つ仮想スクロール(Virtual Scrolling)や、動的に生成されるフィード画面において、安易なセレクタ設計を行うと、スタイル再計算(Style Recalculation)のフェーズでメインスレッドを圧迫する原因となる。
—
3. SPAとCSS Modules時代における `:visited` のアンチパターンと実務的解法
モダンなフロントエンド開発では、CSS Modules、Tailwind CSS、あるいはCSS-in-JS(Emotion, styled-componentsなど)といったコンポーネント指向のスタイリングが主流だ。しかし、これらは `:visited` と非常に相性が悪い。
特にCSS-in-JSの多くは、動的に生成されたクラス名を要素に付与するが、`:visited` のセキュリティ制約(History Sniffing対策として、異なるオリジンや動的なハッシュ衝突を防ぐための厳格なマッチング)により、意図したスタイルが適用されない、あるいはハッシュの解決順序(LVHAルール: Link, Visited, Hover, Active)の崩壊によってスタイリングが上書きされるバグが頻発する。
以下の実用的なコード例を見てほしい。堅牢なアーキテクチャを持つデザインシステムにおける、安全なグローバル・ローカルハイブリッドのスタイル定義だ。
/ =================================================================バグを回避する堅牢なリンクコンポーネントのCSS設計
================================================================= /
/ 1. まずベースとなるリンクスタイルを定義 /
.app-link {
color: var(–color-primary-600);
text-decoration: underline;
transition: color 0.2s ease-in-out;
}
/ 2. ホバーやフォーカスは動的に変化するインタラクション /
.app-link:hover {
color: var(–color-primary-800);
}
.app-link:focus-visible {
outline: 2px solid var(–color-focus-ring);
outline-offset: 2px;
}
/ 3. :visited の定義における鉄則:
- レイアウトプロパティ(margin, padding, transform等)は絶対に書かない
- LVHAの順序(Link -> Visited -> Hover -> Active)を厳守する
/
.app-link:visited {
/ プライバシー保護の制限内である color のみを安全に変化させる /
color: var(–color-visited-purple-700);
}
/ 4. ダークモード等における既読色のコントラスト比担保
※ prefers-color-scheme メディアクエリ内でも :visited は正常に機能するが、
ブラウザのプライバシーバリア(Visited Link Coloringの分離)により、
親要素の色継承(currentColor)との組み合わせで予期せぬ描画バグが起きることがあるため、
明示的にカラー変数を指定するのが安全。
/
@media (prefers-color-scheme: dark) {
.app-link:visited {
color: var(–color-visited-purple-300);
}
}
—
4. 高度なアーキテクチャ戦略:なぜ `:visited` の色変更は最小限に留めるべきか?
シニアアーキテクトとしてプロジェクトを率いる際、デザイナーが「既読リンクの背景色をブロック全体で変えたい」「既読のアイコンだけサイズを大きくしたい」と要求してきたら、即座に「NO」と言えなければならない。
先述した通り、`:visited` に適用できるのは色に関するプロパティのみである。もし背景色を変更する場合、以下のようなブラウザのレンダリング挙動上の罠が存在する。
描画の非同期性とPaintの最適化
ブラウザは、要素が `:visited` であるか否かを通常のスタイル計算パスとは異なる、プライバシー分離されたコンテキストで判定する。そのため、`:visited` による色の変更は、DOM要素の描画木(Paint Tree)の構築時にわずかなペナルティを生むことがある。
特に、数万個のノードを持つ複雑なダッシュボードにおいて、各リンクが個別に `:visited` の判定と色の再描画を行うと、スクロールパフォーマンス(FPS)の低下を招く。
アーキテクチャ上の推奨プラクティス:
1. 影響範囲の限定: `:visited` によるスタイル変更は、テキストリンクの `color` のみに絞る。
2. アイコンの扱い: リンク内に含まれるSVGアイコンの `fill` も `:visited` の影響を受けるが、構造が複雑な場合はCSSでの制御を避け、JavaScript(あるいはNext.jsやRemixなどのSSR/RSC層)で既読状態をサーバーサイドまたはクライアントのキャッシュから明示的に判定し、DOMクラス(例: `is-visited`)を付与するアプローチも検討する(※ただし、これは「ユーザーの完全なプライバシー保護(履歴の隠蔽)」を目的とした `:visited` とは異なり、UX向上としての既読管理であるため要件を精査すること)。
—
結びにかえて
`:visited` は、Webの黎明期から存在する地味な疑似クラスに映るかもしれない。しかし、その内部には「ユーザーのプライバシーを守るためのブラウザエンジンの苦闘」と「セキュリティとパフォーマンスのギリギリのトレードオフ」が凝縮されている。
表面的なAPIの使い方を覚えるだけでなく、ブラウザのメモリ構造、レンダリングのライフサイクル、そしてセキュリティサンドボックスの制約まで見通すこと。それこそが、真に堅牢でモダンなWebアプリケーションを構築するフロントエンド・スペシャリストの視座である。
次にコードを書くときは、その小さな `:visited` の背後で動いているブラウザの息吹に、少しだけ想いを馳せてみてほしい。

コメント