JavaScript
useEffect? useYourBrain!!
原則
JavaScriptはエンジニアのためのマルチツールではありません。
JavaScriptの機能を実行する必要がある場合JavaScriptを使用します。
JavaScript以外で実現できる機能には、JavaScriptを使用しません。
環境
JavaScriptフレームワークを使用する場合はそのフレームワークのドキュメントに従ってください。 静的ウェブサイトを構築する場合、Viteを推奨します。
Biome(またはESLint)に適合させる
設定ファイルは以下です。要件に合わせて調整してください。
monosus/lint-tools
コーディングスタイル全般も確認してください。
TypeScriptのエラーはコミット時に取り除く
lefthookなどでprecommit時にタイプチェックを行うように徹底してください。
プロジェクトとして、CI(継続的インテグレーション)による証跡が必要でない限り、ローカルで静的テスト(リント)を行う形式で十分です。キックオフミーティングなど導入時に、lefthookなどがが全員の環境で機能しているか確認します。
簡単で良いのでDesign docを作成してからコーディングする
Design doc(デザインドキュメント)とは、エンジニアが実装前の設計や考えていることをまとめるためのフレームワークおよびその成果物です。
- 目的
- 概要
- 設計するもの(やりたいこと)
- 考慮した代替案/設計しないもの
- 実装の計画
というアウトラインに沿って制作するものの抽象度を下げていきます。
とはいえ、この章で取り扱うJavaScriptは静的ウェブサイトで用いられるインタラクションなどを主としています。
そのため、重厚なDesign docを用意する必要はなく、GitHubのissueなどに記載するレベルで構いません。何をやろうとしていて何をやらないのかを記述し計画します。
参考:リッチなDesign docの例
最終案に到達するまでに考案した代替案もできる限り記載しましょう。将来そのコードをリファクタリングする場合になぜ別の実装ではなかったのかなどを対応者が把握するのを助けることができます。 また、いくつかの候補から検討したうえで選択した実装は品質が高いものとなるでしょう。将来のためだけでなく、現時点で実装するタイミングでもなぜその様にコーディングしたかを鮮明にしておきましょう。
Design docは未完成のままで他の人と共有して構わないドキュメントです。チームメンバーやレビュアーと積極的に共有してください。ドキュメントどおりのコードになれば最高ですが、最終的な実装がドキュメントと相違していても構いません。無計画にコードを書き始めることがないことが重要です。
参考リソース:メルカリShopsでのDesign Docs運用について | メルカリエンジニアリング
jQueryを使用しない
jQueryは現代のJavaScriptには不要です。我々は工数ではなくプログラムを作成し提供しています。(そしてVanilla JSでも工数は変わりません)
HTML内で単独機能のファイルを読み込まない
ページにはscript.js,entry.js,main.js,about-page.jsの様なエントリーポイントとなるjsファイルを読み込みます。
これはJavaScriptの読み込み順などによる思わぬバグがでないようにするための処置であると同時に、あるページはmain.jsでアコーディオン機能を提供、あるページではaccordion.jsでアコーディオン機能を提供などの揺れがでないようにするためです。
⛔Bad<script src="accordion-helper.js">
👍Good<script type="module" src="entry.js">モダンJavaScriptを使用する
ES6以降の機能を積極的に使用して、コードをより簡潔で読みやすく、保守しやすくします。
ES6以降の機能例
例1:アロー関数
// ⛔Bad:伝統的な関数宣言を使用するfunction add(a, b) { return a + b;}
// 👍Good:アロー関数を使用するconst add = (a, b) => a + b;例2:テンプレートリテラル
javascript// ⛔Bad:文字列連結を使用する
const name = 'John';const greeting = 'Hello, ' + name + '!';
// 👍Good:テンプレートリテラルを使用するconst name = 'John';const greeting = `Hello, ${name}!`;例3:分割代入
// ⛔Bad:オブジェクトのプロパティに個別にアクセスするconst person = { name: 'John', age: 30 };const name = person.name;const age = person.age;
// 👍Good:分割代入を使用するconst person = { name: 'John', age: 30 };const { name, age } = person;例4:スプレッド構文
// ⛔Bad:配列の結合を手動で行うconst arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const combined = arr1.concat(arr2);
// 👍Good:スプレッド構文を使用するconst arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const combined = [...arr1, ...arr2];例5:デフォルトパラメータ
// ⛔Bad:デフォルト値を手動で設定するfunction greet(name) {const actualName = name || 'Guest'; return Hello, ${actualName}!;}
// 👍Good:デフォルトパラメータを使用するfunction greet(name = 'Guest') { return Hello, ${name}!;}カスタムよりもネイティブ
独自のものを作成するのではなく、ネイティブ関数、API、機能を優先して使用します。
// 🤬Not Good:カスタム関数を使用して配列をソートするfunction customSort(arr) { for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { let temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } return arr;}
const numbers = [5, 3, 8, 1, 2];const sortedNumbers = customSort(numbers);console.log(sortedNumbers); // [1, 2, 3, 5, 8]
// 👍Good:ネイティブのsortメソッドを使用して配列をソートするconst numbers = [5, 3, 8, 1, 2];const sortedNumbers = numbers.sort((a, b) => a - b);console.log(sortedNumbers); // [1, 2, 3, 5, 8]// 🤬Not Good:カスタム関数を使用して非同期処理を行うfunction fetchData(url, callback) { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => callback(JSON.parse(xhr.responseText)); xhr.send();}
fetchData('https://api.example.com/data', (data) => { console.log(data);});
// 👍Good:ネイティブのfetch APIを使用して非同期処理を行うfetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data));不要なAPI呼び出しを控える
メモリリークなどに影響があるため、ループ処理などで同一APIを何度も叩くことのないようにします。
// ⛔Bad
for(const item of document.querySelectorAll(".item-list > li")) {/* ... */}
// 👍Good
const listItems = document.querySelectorAll(".item-list > li");
for(const item of listItems){/* ... */}バックエンドやサーバーから取得したデータの保存についてはクライアントと相談する
同一ページで何度もデータベースにアクセスすると、何度もレスポンス時間が発生するためにUX(ユーザーエクスペリエンス)に悪影響がでるかもしれません。

データベースに保存されている情報が必要で、コード内でその情報が複数回必要になる(または要求される可能性がある)場合は、データベースに1回だけアクセスし、後続の処理のためにデータをローカルに保存することが望ましいです。 ただし、どの様にフロントでデータを管理するかについてはクライアントと相談し、合意したうえで対応しましょう。
ユーザーストレージの参考:
- 再訪者に同じデータを使用する目的 → Cookie
- 今回のセッション中のみ使用 → セッションストレージ
- 取得したデータは一般的な内容であり個人情報などセキュアなものを含んでいない、そのデータをある程度永続的に管理したい → ローカルストレージまたはIndexedDB
あとから追加されたDOMにも対応出来るようにデリゲーションする
クライアントと相談し、運用フェーズで接客ツールなどを使用しUI(ユーザーインターフェース)の複製や上書きがLoad時に行われることが見込まれる場合はイベントデリゲーションでの実装を検討してください。
// ⛔Bad:直接イベントリスナーを追加するdocument.querySelectorAll('.dynamic-button').forEach(button => { button.addEventListener('click', () => { console.log('Button clicked!'); });});
// 👍Good:イベントデリゲーションを使用するdocument.addEventListener('click', (event) => { if (event.target.matches('.dynamic-button')) { console.log('Button clicked!'); }});関数の引数はオブジェクト形式で渡す
関数を呼び出す際の見通しはとても重要です。特に関数制作者と使用者が異なる場合に実装間違いやケアレスミスを防ぐためにルールを統一します。 (あなた以外の開発者も対象として)将来拡張される可能性がなく、引数は一つしか受け取らないようであればオブジェクトではない形式で構いません。
// ⛔Bad:複数の引数を個別に渡すfunction createUser(name, age, email) {console.log(Name: ${name}, Age: ${age}, Email: ${email});}createUser('John Doe', 30, 'john.doe@example.com');
// 👍Good:オブジェクト形式で引数を渡すfunction createUser({ name, age, email }) {console.log(Name: ${name}, Age: ${age}, Email: ${email});}createUser({ name: 'John Doe', age: 30, email: 'john.doe@example.com' });エラーハンドリングを盛り込む
エラーハンドリングは、予期しないエラーが発生した際にアプリケーションが適切に対応し、クラッシュを防ぐために重要です。 エラーハンドリングを行うことで、ネットワークエラーやAPIの不具合などに対してログを出したり、スクリプトの失敗がページ全体の問題にならないように設計できます。
例:fetch操作
// ⛔Bad:エラーハンドリングを行わないfunction fetchData(url) { return fetch(url) .then(response => response.json()) .then(data => console.log(data));}fetchData('https://api.example.com/data');// 👍Good:エラーハンドリングを行うfunction fetchData(url) { return fetch(url) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => console.log(data)) .catch(error => console.error('There was a problem with the fetch operation:', error));}
fetchData('https://api.example.com/data');例:DOM操作
// ⛔Bad:エラーハンドリングを行わないfunction updateElementText(id, text) { document.getElementById(id).innerText = text;}
updateElementText('myElement', 'Hello, World!');
// 👍Good:エラーハンドリングを行うfunction updateElementText(id, text) { try { const element = document.getElementById(id); if (!element) { throw new Error(Element with id ${id} not found); } element.innerText = text; } catch (error) { console.error('Error updating element text:', error); }}
updateElementText('myElement', 'Hello, World!');サードパーティスクリプトはクライアントが指定したものだけを実装する
CDNからライブラリを読み込むことは基本的にNGです。
可能な場合は計測スクリプトを遅延読み込みする
ウェブパフォーマンスを阻害する最大のボトルネックはサードパーティ製の計測タグやカスタムスクリプトです。
計測はマーケティング上必須であってもサイトに訪れるユーザーには必要のないものです。
可能であればユーザー体験を優先し、
third-party-script.jsのようなjsファイルを作成し、DOMContentLoadedイベント発火時にを計測タグなどをDOMに挿入する形でレンダリングをブロックさせないようにします。

- クライアントサイドでABテストを行う場合、上記の順番ではテスト前の状態が一度レンダリングされてしまうため適しません
- 遅延と言っても多大に遅れるわけではありません。コーディングのプロがしっかりと設計したウェブページは
DOMContentLoadedまでの時間も最小限になります
この項目については、クライアントと話し合いながらそのプロジェクトにあった方法を採用してください。
ライブラリを使用する場合は容量だけでなくライブラリ内の依存関係も確認する
依存ライブラリが無いライブラリであればアップデート漏れによる不具合などが出る可能性が下がります。
ライブラリを使用しなければ依存関係に悩むことはありません。
また、フロントエンド領域のコーディングであればdevDependenciesで読み込んでいるライブラリの依存関係は一定目を瞑って問題ありません。
module形式で読み込む
module形式で読み込むことで読み込みタイミングがDOMContentLoaded時点とになります。またグローバルスコープに記述が漏れることもなくなります。
import { add, subtract, PI } from './mathUtils.js';
console.log('Add:', add(5, 3)); // 出力:Add: 8console.log('Subtract:', subtract(5, 3)); // 出力:Subtract: 2console.log('PI:', PI); // 出力:PI: 3.14159// 関数をエクスポートexport function add(a, b) { return a + b;}
export function subtract(a, b) { return a - b;}
// 定数をエクスポートexport const PI = 3.14159;<script type="module" src="main.js"></script>Dynamic importを使用し必要なモジュールだけを読み込む
対象の要素やセレクターがあった場合にだけ必要となるモジュールを読み込みます。
サイト全体で使うことがすでに決まっているものは通常のimportで問題ありません。
// ⛔Bad:ページロード時に常に読み込むimport './accordion-helper.js';
// 👍Good:必要な場合にのみ動的に読み込むdocument.addEventListener('DOMContentLoaded', () => { if (document.querySelector('details')) { import('./accordion-helper.js') .then(module => { // モジュールの使用 module.initializeAccordion(); }) .catch(error => { console.error('Error loading accordion-helper.js:', error); }); }});条件分岐のネストを浅くする
例えばif文のネストは最大でも2つまでとし、さらに複雑になるようであれば別の関数や文に切り出して処理の見通しを確保します。
scrollやresizeイベントを使用する前に別の方法を検討する
scrollイベントの代わりにIntersectionObserver
resizeイベントの代わりにResizeObserver
を使用できないか検討してください。パフォーマンスと汎用性の観点からより望ましい選択肢となります。
scrollやresizeイベントを使用する場合はthrottle、debounce処理を組み込む
スクロールにはthrottle、リサイズにはdebounceを組み込みます。
参考リソース:Debounce と Throttle(JavaScript)
// ⛔Bad:直接イベントリスナーを追加するwindow.addEventListener('scroll', () => { console.log('Scroll event fired');});
window.addEventListener('resize', () => { console.log('Resize event fired');});
// 👍Good:throttleを使用するwindow.addEventListener('scroll', throttle(() => { console.log('Scroll event fired');}, 200));
// 👍Good:debounceを使用するwindow.addEventListener('resize', debounce(() => { console.log('Resize event fired');}, 200));debug用のコンソールログを納品ファイルに混ぜない
サイトオーナー側で継続的に開発する場合において、
引き続き開発時に必要なログはその旨のコメントとともにコメントアウトします。
// ⛔Bad:debug用のコンソールログをそのまま残すfunction processData(data) { console.log('Processing data:', data); // デバッグ用// データ処理のコード}
// 👍Good:debug用のコンソールログをコメントアウトするfunction processData(data) { // console.log('Processing data:', data); // デバッグ用 // データ処理のコード}