シニアフロントエンドエンジニア面接質問
シニアフロントエンドエンジニア面接の重点、よく出る質問、そして即時AIフィードバックでの練習方法。
シニアレベルで求められること
アーキテクチャ、パフォーマンス予算、アクセシビリティの責任、メンタリングやテックリードの素養が問われます。
フロントエンドエンジニアの面接質問サンプル
- 技術面接ブラウザのイベントループと、マイクロタスクとマクロタスクの違いを説明してください。良い回答が押さえる点
- コールスタックとタスクキューの関係
- マイクロタスク(Promiseコールバック、MutationObserver)
- マクロタスク(setTimeout、DOMイベント)
- 実行順序:マクロタスク→全マイクロタスク→次のマクロタスク
- requestAnimationFrameの位置づけ
サンプル回答を見る
ブラウザのイベントループは、コールスタックが空になったとき、タスクキューからタスクを取り出して実行します。タスクにはマクロタスクとマイクロタスクの2種類があり、マクロタスクはsetTimeoutやDOMイベントなど、キューごとに1つずつ処理されます。一方、マイクロタスクはPromiseのコールバックやMutationObserverなどで、各マクロタスクの完了後、次のマクロタスクの前にキュー内の全マイクロタスクが実行されます。具体的な順序は、まず1つのマクロタスクを実行し、次にマイクロタスクキューが空になるまで全てのマイクロタスクを処理し、その後ブラウザのレンダリング(必要なら)を行い、再び次のマクロタスクを取得します。requestAnimationFrameはレンダリング前に実行されるため、マイクロタスクとは異なるタイミングです。よくある間違いは、マイクロタスクを大量に追加してイベントループをブロックすることです。これによりレンダリングが遅延し、UIが固まります。
- 技術面接Reactで再レンダリングを引き起こすものは何で、不要な再レンダリングをどう防ぎますか?良い回答が押さえる点
- state/props変更による再レンダリング
- 親コンポーネントの再レンダリングによる子の再レンダリング
- Context値変更による全コンシューマの再レンダリング
- React.memoによるメモ化
- useMemo/useCallbackによる値と関数のメモ化
- keyプロパティの適切な使用
サンプル回答を見る
Reactで再レンダリングを引き起こす主な要因は、stateまたはpropsの変更、親コンポーネントの再レンダリング、Context値の更新です。Reactはデフォルトで親が再レンダリングされると子も再レンダリングします。不要な再レンダリングを防ぐには、React.memoでコンポーネントをラップしてpropsが浅く等しい場合にスキップします。関数やオブジェクトが毎回新しい参照になるのを防ぐため、useCallbackやuseMemoを使用します。また、リストレンダリング時には一意のkeyを指定して差分検出を効率化します。Contextは値を分割して不必要な再レンダリングを避けるか、useContextの代わりにReact.memoと組み合わせます。ただし、メモ化にはオーバーヘッドがあるため、本当に必要な箇所だけに適用すべきです。よくあるミスは、React.memoを多用して逆にパフォーマンスを悪化させることです。
- 技術面接CSSのカスケードが競合するルールをどう解決するか説明してください。良い回答が押さえる点
- 特異度(inline > id > class > 要素)
- !importantによるオーバーライド
- ソース順序(後勝ち)
- 継承と初期値(initial)
- カスケードレイヤー(@layer)
サンプル回答を見る
CSSカスケードは、競合するルールを解決するために以下の優先順位を適用します。まず、ルールの出所(ユーザーエージェント、ユーザー、作成者)で比較されますが、通常は作成者スタイルが優先です。次に特異度(specificity)を計算します。インラインスタイルが最も高く、次にIDセレクタ、class/属性/疑似クラス、要素/疑似要素の順です。特異度が同じ場合は、後から宣言されたルールが優先されます(ソース順序)。!importantが宣言されたルールは特異度を無視して優先されますが、複数ある場合は特異度で再比較されます。また、CSSレイヤー(@layer)を使うと、レイヤーの優先順位を制御できます。継承はすべてのプロパティで行われるわけではなく、例えばwidthは継承しません。初期値を強制したい場合はinitialキーワードを使用します。よくある誤解は、特異度を無視してクラス数だけで判断することです。実際にはIDがクラスより常に強いわけではなく、特異度は3桁の数値で比較されます。
- コーディングdebounce関数を実装し、どんな場面で使うか説明してください。良い回答が押さえる点
- タイマー管理による遅延実行
- leading/trailingオプション
- 実装:setTimeoutとclearTimeout
- 使用例:検索入力、リサイズ、スクロール
- 時間計算量O(1)(呼び出しごと)
サンプル回答を見る
debounce関数は、連続して呼び出される関数を遅延させ、最後の呼び出しから一定時間経過後に実際に実行することで、高頻度のイベントを間引くためのテクニックです。実装の要点は、setTimeoutとclearTimeoutを使ってタイマーを管理し、呼び出されるたびに前のタイマーをキャンセルして新しいタイマーをセットすることです。オプションとして、先頭(leading)エッジで即座に実行し、末尾(trailing)エッジで遅延実行するか指定できるようにするのが一般的です。典型的な使用場面は、検索ボックスへの入力(サーバーリクエストを間引く)、ブラウザのリサイズやスクロールイベントのハンドリングです。時間計算量は呼び出しごとにO(1)で、タイマーの管理以外に重い処理はありません。よくある注意点は、debounceされた関数内でthisのコンテキストが失われることです。そのためアロー関数を使うか、bindで固定します。また、leadingオプションを有効にすると初回の反応が良くなりますが、検索などで不要なリクエストが発生することもあります。
参考コードtypescript /** * debounce関数 - 連続呼び出しを間引いて最後の呼び出しのみ実行 * @param func 実行する関数 * @param delay 遅延時間(ミリ秒) * @param options { leading: 先頭で即時実行するか, trailing: 末尾で遅延実行するか } */ type DebouncedFunction<T extends (...args: any[]) => any> = { (...args: Parameters<T>): void; cancel: () => void; }; export function debounce<T extends (...args: any[]) => any>( func: T, delay: number, options: { leading?: boolean; trailing?: boolean } = { leading: false, trailing: true } ): DebouncedFunction<T> { let timerId: ReturnType<typeof setTimeout> | null = null; let lastArgs: Parameters<T> | null = null; let leadingInvoked = false; const invoke = () => { if (lastArgs !== null && options.trailing !== false) { func(...lastArgs); leadingInvoked = false; // leadingが再度有効になるようにリセット } timerId = null; }; const debounced = (...args: Parameters<T>) => { lastArgs = args; if (timerId === null && options.leading && !leadingInvoked) { // leadingエッジで即時実行 func(...args); leadingInvoked = true; } if (timerId !== null) { clearTimeout(timerId); } timerId = setTimeout(invoke, delay); }; debounced.cancel = () => { if (timerId !== null) { clearTimeout(timerId); timerId = null; } lastArgs = null; leadingInvoked = false; }; return debounced; } // 使用例: // const handleSearch = debounce((query: string) => { // fetchSuggestions(query); // }, 300); // input.addEventListener('input', (e) => handleSearch(e.target.value)); - コーディングキーボード対応のアクセシブルなオートコンプリートコンポーネントを作成してください。良い回答が押さえる点
- WAI-ARIA属性(role='combobox', aria-expanded, aria-activedescendant)
- キーボードナビゲーション(上下矢印、Enter、Escape)
- 状態管理(入力値、候補リスト、フォーカスインデックス)
- 候補フィルタリングとハイライト
- スクリーンリーダー対応(aria-live region)
- エッジケース:空リスト、外部クリック閉じる
サンプル回答を見る
キーボード対応のアクセシブルなオートコンプリートコンポーネントは、WAI-ARIAオーサリングプラクティスに従い、role='combobox'、aria-expanded、aria-activedescendantなどを適切に設定します。キーボードナビゲーションでは、上下矢印で候補を移動し、Enterで選択、Escapeで閉じるようにします。状態として、入力値、フィルタリングされた候補リスト、リストの開閉、フォーカスしているインデックスを持ちます。フィルタリングは入力値に基づいて実行し、候補がない場合はリストを非表示にします。また、外部クリックでリストを閉じる処理や、スクリーンリーダー向けにaria-liveリージョンで候補数を通知します。エッジケースとして、空の入力時や候補が0件の場合の表示、初回入力時の即時フィルタリングなどに対応します。さらに、非同期で候補を取得する場合は、debounceでリクエストを間引くとパフォーマンスが向上します。
参考コードtypescript import React, { useState, useRef, useEffect, useCallback, KeyboardEvent, ChangeEvent } from 'react'; interface AutocompleteProps { suggestions: string[]; onSelect: (value: string) => void; placeholder?: string; } const Autocomplete: React.FC<AutocompleteProps> = ({ suggestions, onSelect, placeholder }) => { const [inputValue, setInputValue] = useState(''); const [filteredSuggestions, setFilteredSuggestions] = useState<string[]>([]); const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const inputRef = useRef<HTMLInputElement>(null); const listRef = useRef<HTMLUListElement>(null); // 入力値が変わったら候補フィルタリング useEffect(() => { const filtered = suggestions.filter(s => s.toLowerCase().includes(inputValue.toLowerCase()) ); setFilteredSuggestions(filtered); setIsOpen(inputValue.length > 0 && filtered.length > 0); setActiveIndex(-1); }, [inputValue, suggestions]); const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setInputValue(e.target.value); }; const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); if (isOpen) { setActiveIndex(prev => (prev < filteredSuggestions.length - 1 ? prev + 1 : 0)); } break; case 'ArrowUp': e.preventDefault(); if (isOpen) { setActiveIndex(prev => (prev > 0 ? prev - 1 : filteredSuggestions.length - 1)); } break; case 'Enter': e.preventDefault(); if (activeIndex >= 0 && filteredSuggestions[activeIndex]) { selectSuggestion(filteredSuggestions[activeIndex]); } break; case 'Escape': e.preventDefault(); setIsOpen(false); setActiveIndex(-1); inputRef.current?.blur(); break; } }; const selectSuggestion = useCallback((value: string) => { setInputValue(value); setIsOpen(false); setActiveIndex(-1); onSelect(value); inputRef.current?.focus(); }, [onSelect]); // 外部クリックで閉じる useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (inputRef.current && !inputRef.current.contains(e.target as Node) && listRef.current && !listRef.current.contains(e.target as Node)) { setIsOpen(false); setActiveIndex(-1); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return ( <div style={{ position: 'relative' }}> <label htmlFor="autocomplete-input">検索</label> <input id="autocomplete-input" ref={inputRef} type="text" role="combobox" aria-expanded={isOpen} aria-controls="autocomplete-list" aria-activedescendant={activeIndex >= 0 ? `autocomplete-item-${activeIndex}` : undefined} aria-autocomplete="list" value={inputValue} onChange={handleChange} onKeyDown={handleKeyDown} onFocus={() => { if (filteredSuggestions.length > 0 && inputValue) setIsOpen(true); }} placeholder={placeholder} /> {isOpen && filteredSuggestions.length > 0 && ( <ul id="autocomplete-list" ref={listRef} role="listbox" aria-label="候補" style={{ position: 'absolute', border: '1px solid #ccc', listStyle: 'none', padding: 0, margin: 0, background: 'white', zIndex: 1000 }} > {filteredSuggestions.map((suggestion, index) => ( <li key={suggestion} id={`autocomplete-item-${index}`} role="option" aria-selected={index === activeIndex} onClick={() => selectSuggestion(suggestion)} onMouseEnter={() => setActiveIndex(index)} style={{ padding: '4px 8px', cursor: 'pointer', background: index === activeIndex ? '#eee' : 'transparent' }} > {suggestion} </li> ))} </ul> )} {/* スクリーンリーダー用のライブリージョン */} <div aria-live="polite" role="status" className="sr-only"> {isOpen ? `${filteredSuggestions.length}件の候補があります。` : ''} </div> </div> ); }; export default Autocomplete; - システム設計リアルタイム共同編集ドキュメントエディタのフロントエンドアーキテクチャを設計してください。良い回答が押さえる点
- 要件:低レイテンシ、競合解決、オフライン対応
- 通信方式:WebSocket / WebRTC
- 競合解決アルゴリズム:OT(Operational Transformation) vs CRDT(Conflict-free Replicated Data Types)
- フロントエンドの状態管理(ローカルオペレーションキュー、Undo/Redo)
- スケーラビリティ:ドキュメント分割(シャーディング)、ブロードキャストチャネル
- データモデル:リッチテキスト(Quill, Slate)またはプレーンテキスト
サンプル回答を見る
リアルタイム共同編集ドキュメントエディタのフロントエンドアーキテクチャは、まず要件として低レイテンシ(100ms未満)での同期、競合解決、オフライン編集のサポートが挙げられます。通信にはWebSocketを用いてサーバーと双方向接続を確立し、必要に応じてWebRTCでP2P通信を補完します。競合解決アルゴリズムとしては、OT(Operational Transformation)かCRDT(Conflict-free Replicated Data Types)を選択します。OTはGoogle Docsで使われ、操作を変換して整合性を保ちますが、中央サーバーが必要で複雑です。CRDT(例:Yjs)はピアごとに状態をマージでき、耐障害性が高いですが、メモリ使用量が増加します。フロントエンドでは、ローカルの操作をキューに保持し、サーバーからの確認を受け取るまで待ちます。Undo/Redoはローカルヒストリーで管理します。スケーラビリティとしては、ドキュメントを複数のサーバーにシャーディングし、広域ではブロードキャストチャネルでリアルタイム更新を配信します。データモデルにはQuillやSlateのようなリッチテキストエディタを利用し、操作をJSON形式で表現します。よくある課題は、ネットワーク遅延によるカーソル位置のずれであり、オプティミスティックUIと差分表示で対処します。
- 行動面接遅いページのパフォーマンスを改善した経験を教えてください。良い回答が押さえる点
- STARフレームワーク(状況、タスク、行動、結果)
- 具体的な指標(FPS、ロード時間、Lighthouseスコア)
- 使用したツール/テクニック(Lighthouse、React Profiler、コード分割、仮想化)
- チーム作業やトレードオフの説明
- 学んだ教訓
サンプル回答を見る
以前、自社の管理画面で一覧ページの表示が非常に遅いという問題がありました。Lighthouseで測定したところ、初回ロードに約5秒かかり、Lighthouseのパフォーマンススコアは35でした(状況)。タスクは、ページの読み込み時間を3秒未満に改善し、スコア80以上を目指すことでした(タスク)。私はまず、React Profilerでレンダリングのボトルネックを特定し、巨大なテーブルが原因と判明しました。そこで、react-virtualizedを使って行を仮想化し、表示されている行だけを描画するようにしました。また、画像の遅延読み込み(loading='lazy')、コード分割(React.lazy+Suspense)、不要なライブラリの削除も行いました。さらに、状態管理をContextから個別のuseStateに分割し、不必要な再レンダリングを減らしました(行動)。結果として、初回ロードが1.5秒に短縮され、Lighthouseスコアは95に向上しました。ユーザーからも操作が快適になったと好評でした。この経験から、パフォーマンス改善はプロファイリングに基づいて行うこと、小さな変更の積み重ねが効果的であることを学びました。
- 行動面接UXのトレードオフをめぐるデザイナーとの意見の相違をどう扱いますか?良い回答が押さえる点
- STARフレームワーク
- 具体的な例(例:アニメーションの多さ)
- トレードオフの明確化(視覚的魅力 vs パフォーマンス、アクセシビリティ)
- データやユーザーテストに基づく議論
- 協力的な解決策(A/Bテスト、プログレッシブエンハンスメント)
サンプル回答を見る
以前、モバイル向けのECサイトで、デザイナーが商品一覧に派手なトランジションアニメーションを追加したいと提案しました。私は、低スペック端末でのパフォーマンス低下やアクセシビリティへの影響を懸念しました(状況)。そこで、私はアニメーションの実装コストとパフォーマンスへの影響を数値化し、デザイナーと共有しました。具体的には、CSSアニメーションのフレームレート低下や、prefers-reduced-motion設定を持つユーザーへの配慮を説明しました(タスク)。私は代替案として、アニメーションをCSSのtransformとopacityに限定し、強度を抑えたバージョンを提案するとともに、A/Bテストでユーザーエンゲージメントを比較しようと提案しました(行動)。最終的に、デザイナーも納得し、アニメーションを軽量化して本番リリースしました。テストの結果、アニメーション有り無しでコンバージョン率に有意差はなく、パフォーマンスが向上したためチームとして成功体験を得られました。この経験から、トレードオフをデータで示すことと、ユーザー体験を軸にした建設的な議論が重要だと学びました(結果)。
面接官が評価するポイント
JavaScriptの基礎
クロージャ、イベントループ、Promise/async、プロトタイプ、`this`のバインディング。
フレームワークの深さ
React(またはVue/Angular)のレンダリング、再調整、フック、状態管理。
CSSとレイアウト
Flexbox/grid、カスケード、スタッキングコンテキスト、レスポンシブデザイン。
パフォーマンス
クリティカルレンダリングパス、バンドルサイズ、遅延読み込み、Core Web Vitals。
アクセシビリティ
セマンティックHTML、ARIA、キーボードナビゲーション、スクリーンリーダー対応。
準備の進め方
- レンダリングパスを声に出して説明する — 面接官は答えだけでなく推論を採点します。
- 明示的に尋ねられなくても、アクセシビリティとパフォーマンスに必ず触れましょう。
- IDEの自動補完なしで、1つのコンポーネントを最初から最後まで作る練習をしましょう。
よくある質問
フロントエンド面接でよく出るコーディング問題は?
DOM操作、debounce/throttleのようなユーティリティ関数、オートコンプリートやモーダルのような小さなインタラクティブコンポーネントの構築が出ます。
フロントエンド面接にシステム設計は含まれますか?
ミドルおよびシニアレベルでは含まれます。通常はバックエンドインフラではなく、フィード、デザインシステム、共同編集エディタなどフロントエンド寄りの設計です。
フロントエンド面接に短期間で備えるには?
JavaScriptの基礎を鍛え、いくつかのコンポーネントをゼロから作り、時間制限付きの模擬面接で、プレッシャー下でも推論を説明できるようにしましょう。
フロントエンドエンジニアの質問をAIの即時フィードバックで練習
Offerslyはあなたの履歴書と希望職種に合わせた模擬面接を実施し、すべての回答を関連性・深さ・明確さ・正確さで採点します。