12. パフォーマンス・アクセシビリティ・セキュリティ

この章では、JavaScriptの品質について学びます。

ここまでの章では、UIを動かす方法、フォームを扱う方法、アニメーションやスライダーを学んできました。

しかし、実務のJavaScriptは「動けばOK」ではありません。

重すぎないか、キーボードで操作できるか、スクリーンリーダーに状態が伝わるか、危険な値の扱いをしていないか。こうした視点も、納品物の品質に関わります。

パフォーマンスとは

パフォーマンスは、サイトの表示速度や操作の軽さに関わります。

JavaScriptが重いと、次のような問題が起きます。

  • ページ表示が遅くなる
  • ボタンを押しても反応が遅い
  • スクロールがカクつく
  • スマホで操作しづらい
  • アニメーションが不自然になる

特にWeb制作では、画像やWebフォント、CSS、外部タグも重さに影響します。

JavaScriptだけ見ればよいわけではありませんが、JavaScriptが重くなりすぎないようにする意識は必要です。

JavaScriptを読み込みすぎない

ライブラリは便利ですが、増やせば増やすほど読み込み量が増えます。

よくある積み重なり
main.js
gsap.js
scrolltrigger.js
splide.js
validation.js
map-widget.js
外部チャットタグ
予約ウィジェットタグ
SNS埋め込みタグ

1つ1つは小さく見えても、積み重なると重くなります。

defer

JavaScriptの読み込み位置や実行タイミングも重要です。

defer付きscript
<script src="/assets/js/main.js" defer></script>

deferを付けると、HTMLの解析を妨げにくくなり、HTMLの解析後にJavaScriptが実行されます。

多くのWeb制作案件では、共通JavaScriptをdefer付きで読み込む構成が扱いやすいです。

DOM操作を減らす

DOM操作は便利ですが、何度も大量に行うと重くなることがあります。

避けたい例です。

避けたい例
items.forEach((item) => {
    document.querySelector(".js-count").textContent = item.textContent;
});

ループのたびに同じ要素を探しています。

先に取得しておくと読みやすくなります。

改善例
const countText = document.querySelector(".js-count");

items.forEach((item) => {
    countText.textContent = item.textContent;
});

実際にはこの例もロジックとしては不自然ですが、考え方としては「何度も同じDOM取得をしない」ことが大切です。

scrollイベントを重くしない

scrollイベントは、スクロール中に何度も発火します。

スクロールイベント
window.addEventListener("scroll", () => {
    const isScrolled = window.scrollY > 100;

    header.classList.toggle("is-scrolled", isScrolled);
});

この程度なら問題になりにくいですが、スクロールのたびに大量の要素を取得したり、高さを計算したり、重い処理をするとカクつきます。

要素が画面に入ったかどうかを見たいだけなら、Intersection Observerを使う方が向いています。

アニメーションはtransformとopacity中心

アニメーションでは、できるだけtransformopacityを中心に使います。

扱いやすいアニメーション
.fade-in {
    opacity: 0;
    transform: translateY(24px);
    transition:
        opacity 0.4s ease,
        transform 0.4s ease;
}

widthheighttopleftなどを頻繁に動かすと、レイアウト計算が増えやすくなります。

もちろん絶対に使ってはいけないわけではありませんが、基本はtransformopacityを優先します。

Lighthouseで確認する

ChromeのLighthouseを使うと、パフォーマンスやアクセシビリティのヒントを確認できます。

ただし、スコアだけを追いかけすぎるのは危険です。

アクセシビリティとは

アクセシビリティは、できるだけ多くの人が情報や機能にアクセスできるようにする考え方です。

JavaScriptでUIを作る時は、マウスでクリックできるだけでは不十分です。

  • キーボードで操作できるか
  • フォーカス位置がわかるか
  • 開閉状態が支援技術に伝わるか
  • エラー状態が入力欄と関連付いているか
  • 動きが苦手な人に配慮しているか

こうした点を確認します。

ボタンとリンクを正しく使う

クリックできるUIを作る時、何でもdivにしてはいけません。

避けたい例
<div class="js-menu-button">メニュー</div>

見た目はボタンのようにできますが、キーボード操作や意味の面で不十分です。

良い例
<button class="js-menu-button" type="button">メニュー</button>

ページ遷移するものはa、その場で状態を変えるものはbuttonを使います。

aria-expanded

aria-expandedは、開閉できる要素が開いているかどうかを表します。

HTML
<button class="js-accordion-button" type="button" aria-expanded="false">
    質問
</button>
aria-expandedを更新する
const isOpen = button.getAttribute("aria-expanded") === "true";

button.setAttribute("aria-expanded", String(!isOpen));

アコーディオン、メニュー、ドロップダウンなどで使います。

aria-hidden

aria-hiddenは、支援技術に対して要素を隠すかどうかを表します。

aria-hidden
menu.setAttribute("aria-hidden", "true");

ただし、見た目の表示非表示とは別です。

CSSやhidden属性と組み合わせて、実際の表示状態とも矛盾しないようにします。

aria-invalidとaria-describedby

フォームのエラーでは、aria-invalidaria-describedbyをよく使います。

HTML
<input
    id="email"
    class="js-email-input"
    type="email"
    aria-describedby="email-error"
/>
<p id="email-error" class="js-email-error" hidden></p>
エラー状態
emailInput.setAttribute("aria-invalid", "true");
emailError.hidden = false;
emailError.textContent = "メールアドレスを入力してください。";

入力欄とエラーメッセージが関連付くようにします。

フォーカス管理

モーダルやメニューを開閉する時は、フォーカス位置も考えます。

閉じた後にボタンへ戻す
function closeModal() {
    modal.hidden = true;
    openButton.focus();
}

モーダルを閉じた後、フォーカスがどこかへ消えると、キーボード操作中のユーザーが迷います。

実務のモーダルでは、開いている間のフォーカストラップも検討します。

prefers-reduced-motion

動きが苦手なユーザーのために、アニメーションを減らす設定を考慮します。

CSS
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
    }
}

JavaScript側で大きな演出を実行しないようにすることもできます。

JavaScript
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

if (!prefersReducedMotion) {
    // アニメーションを実行する
}

セキュリティの入口

フロントエンドのJavaScriptでも、危険な書き方があります。

特に注意したいのは、ユーザー入力をHTMLとして扱うことです。

危険な例
message.innerHTML = input.value;

ユーザーが入力した文字列にHTMLやスクリプトが含まれていた場合、XSSと呼ばれる問題につながる可能性があります。

単純に文字として表示したいなら、textContentを使います。

安全寄りの例
message.textContent = input.value;

APIキーをフロントに置かない

JavaScriptファイルは、ブラウザから見えます。

そのため、秘密にするべきAPIキーやトークンをフロント側に書いてはいけません。

避ける例
const secretApiKey = "sk_xxxxxxxxxxxxx";

ブラウザで見えるコードに書いた情報は、基本的にユーザーにも見えると考えます。

公開してよいキーと、秘密にすべきキーを混同しないようにしましょう。

フロントバリデーションだけでは不十分

フォームの章でも触れた通り、JavaScriptの入力チェックだけでは安全になりません。

ユーザーは、ブラウザのJavaScriptを無効化したり、開発者ツールで値を書き換えたりできます。

reCAPTCHAやhoneypot

問い合わせフォームでは、スパム対策としてreCAPTCHAやhoneypotを使うことがあります。

honeypotは、通常のユーザーには見えない入力欄を用意し、botがそこへ入力した場合にスパムと判定する考え方です。

ただし、スパム対策はサイトの要件やフォームシステムによって変わります。

この教材では詳しく扱いませんが、フロントだけで完全に防げるものではないと理解しておきましょう。

よくある失敗

品質面でよくある失敗を整理します。

  • ライブラリや外部タグを増やしすぎて重くなる
  • スクロールイベントに重い処理を入れてカクつく
  • divをボタン代わりにしてキーボード操作できない
  • 見た目だけ切り替えて、aria-expandedなどの状態を更新していない
  • モーダルを閉じた後のフォーカス位置を考えていない
  • ユーザー入力をinnerHTMLに入れてしまう
  • 秘密にすべきAPIキーをフロントに書いてしまう
  • フロント側の入力チェックだけで安全だと思ってしまう

品質チェックリスト

納品前に、次の観点で確認しましょう。

  • Consoleエラーが出ていないか
  • 対象要素がないページでもエラーにならないか
  • スマホでスクロールやタップが重くないか
  • キーボードだけで操作できるか
  • フォーカス位置が見えるか
  • 開閉状態やエラー状態が属性にも反映されているか
  • ユーザー入力をinnerHTMLに入れていないか
  • 秘密情報をフロントに書いていないか
  • Lighthouseなどで大きな問題が出ていないか

この章のまとめ

この章では、JavaScriptの品質に関わる基本を学びました。

  • JavaScriptは、表示速度や操作感に影響する
  • ライブラリは便利だが、必要性を判断して使う
  • スクロールイベントやDOM操作は重くしすぎない
  • アニメーションはtransformopacity中心に考える
  • UIはマウスだけでなくキーボードでも操作できるようにする
  • 開閉状態やエラー状態は、ARIA属性やhiddenにも反映する
  • ユーザー入力をinnerHTMLに入れない
  • フロント側だけで安全性を完結させない

次の章では、JavaScriptでエラーが出た時に、自分で原因を探すためのデバッグ方法を学びます。