Web制作の技術は日々進化しており、会社やプロジェクトによっては昨今の環境に適さない書き方をしているケースも時折見受けられます。
そこで今回は「2024年のWeb制作ではこのようにコードを書いてほしい!」という内容をまとめました。
質より量で、まずは「こんな書き方があるんだ」をこの記事で伝えたかったので、コードの詳細はあまり解説していません。なので、具体的な仕様などを確認したい方は参考記事を読んだりご自身で調べていただけると幸いです。
!当記事では「iOS Safari バージョン15系以上」でサポートされている技術を基本的に紹介しています。しかし、15系や16系でサポートされていない技術も少しだけ含まれているので、その場合は補足をしておりますのでご留意ください。
画像周りはサイトパフォーマンスに直結するので、まずはそこだけでも取り入れていただきたいです。また、コアウェブバイタルやアクセシビリティも併せて理解しておきたい内容です。
https://zenn.dev/necscat/articles/cdd4c17d52f1bc
https://haniwaman.com/wcag-html/
<img>にloading="lazy"属性を付けると画像が遅延読み込みになり、サイトの読み込み時間が早くなります。
<imgsrc="..."alt=""width="600"height="400"loading="lazy">https://gmotech.jp/semlabo/seo/blog/lazyload/
loading="lazy"属性の補足です。
widthとheightが必須(レイアウトシフト対策にもなるので必ず付けましょう)iframe要素にも使えるdecoding="async"はあまり意味がないhttps://zenn.dev/ixkaito/articles/deep-dive-into-decoding
画面幅に応じて画像を出し分ける時は<picture>を使います。
CSS側(display: noneなど)で画像を出し分けると、小さい画面幅の時には不要な「大きい画面幅用の画像」も読み込まれるのでサイトパフォーマンスが悪くなります。
<picture><sourcemedia="(min-width:768px)"srcset="lerge.png"width="400"height="200"><imgsrc="small.png"alt=""width="80"height="40"></picture>https://catnose.me/learning/html/picture
アコーディオンの実装には<details>を使います。ページ内検索で閉じているアコーディオンの中身もヒットしたり、開閉処理が備え付けられているなどのメリットがあります。
<details><summary>タイトル</summary> アコーディオンの中身</details>開閉処理のアニメーションには、GSAPやgrid-template-rowsを使った方法があります。
https://ics.media/entry/220901/
https://www.tak-dcxi.com/article/accordion-slide-animation-can-be-implemented-in-two-line-of-css
モーダルウィンドウの実装には<dialog>を使います。アクセシビリティに優れていたり、z-indexを使わなくても最上位に表示されるなどのメリットがあります。
<dialogopen><div>モーダルのコンテンツ</div><formmethod="dialog"><button>閉じる</button></form></dialog>https://www.tak-dcxi.com/article/implementation-example-of-a-modal-created-using-the-dialog-element
iOS Safariのバージョン15.3以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
見出しに複数の要素(主題+副題)がある場合は<hgroup>でグルーピングします。
<hgroup><h2>DX支援事業</h2><p>経営課題をDXで解決</p></hgroup>https://www.tak-dcxi.com/article/use-hgroup-for-marking-up-the-main-heading-and-subheading
<dl>の直下には<div>を置き、その直下に<dt>と<dd>を置くことでスタイリングがしやすくなります。最新のWHATWGの仕様では<dl>の直下に<div>を置けるようになっています(<div>を置かなくても仕様的には問題ありません)。
<dl><div><dt>クラウドコンピューティング</dt><dd>インターネット経由でコンピューターの資源を提供する...</dd></div><div><dt>API</dt><dd>Application Programming Interfaceの略で、ソフトウェア間で...</dd></div></dl><dl><dt>クラウドコンピューティング</dt><dd>インターネット経由でコンピューターの資源を提供する...</dd><dt>API</dt><dd>Application Programming Interfaceの略で、ソフトウェア間で...</dd></dl><button>はフォーム送信ボタンをマークアップする際に使いますが、フォーム以外の部分でも使えます。
「要素をクリックした時に特定の処理を実行する」のような処理を実装する場合、クリック対象の要素は<button>か<a>を使います。たまに<div>や<p>を使っているコードを見かけますが、本来クリックできない要素にクリック処理を施すと、ブラウザによってはクリックやタップが反応しなかったり、フォーカスが当たらなかったりなどのデメリットが生じます。
<buttontype="button"id="js-trigger-button">ボタン</button><pid="js-trigger-button">ボタン</p>type="button"を付けることで<button>デフォルトの挙動の送信処理がストップします。
サイト内検索や絞り込みを行うフォームの実装には<search>を使うことで、スクリーンリーダーなどに「検索フォーム」ということを指し示せるようになります。
<search><formaction="/search"><labelfor="query">サイト内を検索</label><inputtype="search"name="q"id="query"><buttontype="submit">検索</button></form></search>https://azukiazusa.dev/blog/the-search-element-has-been-added-to-the-html-specification/
iOS Safariのバージョン16.7以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
https://caniuse.com/mdn-html_elements_search
アクセシビリティ向上の目的でW3Cが定めているWAI-ARIAという仕様の中にrole属性とaria属性があります。これらを駆使することでコンテンツの構造や機能に関する情報をスクリーンリーダーなどに適切に伝えることができます。
https://zenn.dev/yusukehirao/articles/e3512a58df58fd
https://ics.media/entry/230821/
https://zenn.dev/moneyforward/articles/b5c9b060cf9237
以下はアクセシビリティを意識したタブの実装例です。
<divrole="tablist"><ahref="#tab-panel-1"id="tab1"role="tab"aria-controls="tab-panel-1"aria-selected="true"tabindex="0">タブ1</a><ahref="#tab-panel-2"id="tab2"role="tab"aria-controls="tab-panel-2"aria-selected="false"tabindex="-1">タブ2</a></div><divid="tab-panel-1"role="tabpanel"aria-labelledby="tab1"tabindex="0"> コンテンツ1</div><divid="tab-panel-2"role="tabpanel"aria-labelledby="tab1"tabindex="0"> コンテンツ2</div><div><ahref="#tab-panel-1"class="active">タブ1</a><ahref="#tab-panel-2">タブ2</a></div><divid="tab-panel-1"> コンテンツ1</div><divid="tab-panel-2"> コンテンツ2</div>https://baigie.me/engineerblog/building-accessible-tabs/
優先的に読み込みたいリソースがある場合は<link>のrel="preload"を使います。例えば、ファーストビューの画像や動画の表示が遅い時はrel="preload"で優先的に読み込んでみると改善する可能性があります。
<linkrel="preload"href="mv.webp"as="image"type="image/webp"/>https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/rel/preload
https://zenn.dev/hrbrain/articles/7f1d1d45f027c7#画像をプリロードする
CDN(Contents Delivery Network)でプラグインなどの外部ファイルを読み込むと「キャッシュサーバーを利用できるので読み込みが早くなる」という理由でよく使われていましたが、CDNで読み込むのは非推奨です。
!CDNのサーバーに障害が発生したり、CDNの提供自体が終了したらプラグインが動かなくなるリスクがあります。
参考:UNPKGの障害によって影響を受けたmicroCMSの投稿
NPMを使える環境の場合、NPMで読み込むことを推奨します。使えない場合はファイルをダウンロードして同プロジェクト内に置いて読み込みましょう。
<scriptsrc="/js/bundle.js"></script><scriptsrc="https://cdn.jsdelivr.net/..."></script>SVGの色をCSS側で変えたい時はインラインで埋め込むと思います。その際にSVGのコードをそのままHTMLに埋め込むのではなく、SVGを<symbol>に変換して別ファイルに保存し、それを<use>で呼び出します。こうすることでSVGの記述量が少なくなるのでHTMLの可読性が高まります。
また、<svg>のwidthheightfillの属性を削除したほうがCSSで扱いやすくなります。
<divclass="icon"><svg><usexlink:href="img/arrow.svg#arrow"></use></svg></div><!-- img/arrow.svg --><symbolid="arrow"viewBox="0 0 24 24"><pathd="M0 0h24v24H0z"fill="none"/><pathd="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></symbol><divclass="icon"><svgviewBox="0 0 24 24"width="24"height="24"><pathd="M0 0h24v24H0z"fill="none"/><pathd="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"fill="red"/></svg></div>モダンなCSSを取り入れるなら、まずはレイアウト手法のGrid Layoutに慣れることからスタートするといいでしょう。また、CSSは特にブラウザのサポート状況が複雑なので、Can I use...などでしっかりと確認してから実務に取り入れてください。
記事一覧などの格子状のレイアウトはGrid Layoutで実装します。Flexboxに比べ、レスポンシブ時に要素の順番や大きさが変わるケースにも対応ができたり、calc()を使った横幅や余白の複雑な計算も不要になります。
.grid{display: grid;grid-template-columns:repeat(3,1fr);/* 3列に並べる */gap:40px;/* 子要素の上下左右の間隔 */}.grid{display: grid;grid-template-areas:"thumb title""thumb description";/* 付けた名前を並べる */grid-template-columns:300px1fr;/* 1列目は300px、2列目は余った幅全て */}/* gridの子要素 */.grid_title{grid-area: title;/* 名前を付ける */}.grid_description{grid-area: description;}.grid_thumb{grid-area: thumb;}https://ics.media/entry/15649/
https://zenn.dev/kagan/articles/4f96a97aadfcb8
Grid Layoutで並べた各アイテム内の要素の縦位置を揃えたい時にSubgridを使います。こちらの例では、説明文の高さがバラバラでも日付の縦位置が同じ位置になるように実装しています。

.grid{display: grid;grid-template-columns:repeat(3,1fr);gap:40px;}.card{display: grid;grid-template-rows: subgrid;grid-row: span3;gap:20px;}https://zenn.dev/tonkotsuboy_com/articles/css-subgrid-all-browsers
iOS Safariのバージョン15.8以下がサポート外なので、プロジェクトの要件に満たしているかを必ず確認しましょう。
https://caniuse.com/css-subgrid
Flexboxで横並びにした要素の余白を調整するならgapプロパティを使います。marginを使うと、calcや◯◯-of-typeなどの記述が発生するので複雑になってしまいます。
.flex{display: flex;gap:20px;}.flex{display: flex;}.child{margin-left:20px;}.child:first-of-type{margin-left:0;}便利な擬似クラスがここ数年で追加されました。
| 擬似クラス | 解説 | 備考 |
|---|---|---|
:has() | 引数に指定した子孫要素を持つ場合、自分自身にマッチ | iOS Safari バージョン15.3以下ではサポート外 |
:is() | 引数に指定した要素にマッチ | 詳細度は通常計算 |
:where() | 引数に指定した要素にマッチ | 詳細度が常に0になる |
https://b-risk.jp/blog/2023/06/new-selector/
:has()を使うことで、CSSだけで子要素の有無に応じてスタイルを変えられます(これまではJSを使っていました)。
/* .card の中に a が含まれているなら背景を赤に、含まれていないなら青にする */.card{background-color:blue;}.card:has(a){background-color:red;}:is():where()を使うことで、親要素や前方隣接要素の状態に応じた記述が楽になります。
.post:is(h2, h3, h4, h5, h6){font-weight: bold;}.post h2,.post h3,.post h4,.post h5,.post h6{font-weight: bold;}Sassでも便利な使い方があります。以下はラジオボタンの選択状態に応じて背景色を変える例で、:is()を使うことでspanのブロック内にスタイルをまとめています。
.radio{span{background-color:blue;// 未選択の時 &:is(input:checked+ span){background-color:red;// 選択済みの時}}}.radio{span{background-color:blue;// 未選択の時}input:checked + span{background-color:red;// 選択済みの時}}https://zenn.dev/kagan/articles/css-is-where-tips
background-sizeプロパティの挙動を使うために画像をbackground-imageプロパティで読み込むのは古い手法です。昨今では<img>で読み込んだ画像に対してobject-fitを使うことで、background-sizeと全く同じ挙動を再現できます。
<img>を使えば遅延読み込みなどの恩恵を受けられるので、画像はできるだけ<img>で読み込むようにしましょう。
.img{width:100px;height:100px;object-fit: cover;}.img{width:100px;height:100px;background-image:url(...);background-size: cover;}画像の比率を制御するにはaspect-ratioプロパティを使います。padding-topを%で指定する昔ながらの手法もありますが、aspect-ratioのほうが記述が簡潔で分かりやすいです。
.img{width:100px;height:100px;aspect-ratio:16/9;/* 縦横比を16:9に */object-fit: cover;/* coverを指定しないと画像の縦横比が崩れる */object-fit-position: center top;/* 必要に応じて画像の位置を調整 */}.parent{position: relative;padding-top:56.25%;/* 56.25% = 16:9 (9/16*100%) */}.child{position: absolute;top:0;left:0;width:100%;height:100%;}親要素全体に自身のサイズを広げる場合、insetプロパティを使うと記述が簡潔になります。insetはtopleftrightbottomを一括指定するショートハンドプロパティです。
.element{position: absolute;inset:0;margin: auto;}.element{position: absolute;top:0;left:0;right:0;bottom:0;margin: auto;}横幅を指定している要素の左右中央寄せはmargin-inline: autoを使います。
/* margin-inlineを使った書き方 */.element{margin-inline: auto;}.element{margin-left: auto;margin-right: auto;}.element{margin:0 auto;}https://zenn.dev/tonkotsuboy_com/articles/margin-inline_auto
横幅を指定しない要素の左右中央寄せはplace-content: centerを使います。
.parent{display: grid;place-content: center;}.parent{display: flex;justify-content: center;align-items: center;}https://zenn.dev/tonkotsuboy_com/articles/css-grid-centering
width: fit-contentを指定すると自身の横幅が子要素の横幅と同じ値になります。つまり、widthに固定値を指定しなくてもmarign-inline: autoなどで中央配置できるようになります。
/* ひとつの要素で中央配置が可能に! */.target{width: fit-content;marign-inline: auto;}.parent{text-align: center;}.target{display: inline-block;}https://iwacode.i-design-creative.com/css-fit-content/
文字列がはみ出ないように折り返しをword-break: break-wordで制御している方は多いと思いますが、現在は非推奨です。
文字列の折り返しについてはICSさんの記事で丁寧に解説されているので是非ご覧ください。
https://ics.media/entry/240411/
body{overflow-wrap: anywhere;word-break: normal;line-break: strict;}body{word-break: break-word;}transformプロパティのtranslateやrotateは独立プロパティになったので、以下のように指定できます。
.element{translate:10px;scale:1.5;rotate:45deg;}https://ics.media/entry/230309/
複数の変形を行っている場合の記述も簡潔になります。
.icon{translate:10px;rotate:45deg;}a:hover.icon{rotate:90deg;}.icon{transform:translate(10px)rotate(45deg);}a:hover.icon{transform:translate(10px)rotate(90deg);}transitionプロパティを使う時はアニメーションを適用させたいプロパティを必ず指定します。
プロパティを指定しないでtransition: all 0.3sのようにすると全てのプロパティにアニメーションが適用されるので、ページ読み込み時やレスポンシブ時に変な挙動になることがあります。
.fadein{transition: opacity0.3s;}.fadein{transition:0.3s;}filterプロパティを使うことで、画像をぼかしたり暗くしたりすることができます。hover時に画像をぼかすような処理も、ぼかし用の画像に切り替えるのではなくCSSだけで完結するので、画像が運用時に変わってもぼかし用の画像作成が不要になります。
.photo{filter:blur(10px);}https://ics.media/entry/15393/#ボカシを使った表現
Figmaなどのデザインツールの機能にある描画モード(乗算、スクリーン、オーバーレイなど)をブラウザ上でも再現できるのがmix-blend-modeプロパティです。filterプロパティと同様に、元画像に手を加えずに加工ができるので運用が楽になります。
.photo{mix-blend-mode: overlay;}三角形などの図形を描画するにはclip-pathプロパティを使います。三角形を作るにはborderを使った昔ながらの手法がありますがclip-pathのほうが直感的に扱えます。
.triangle{clip-path:polygon(100%50%,00,0100%);width:100px;height:100px;background-color:red;}.triangle{width:0;height:0;border-style: solid;border-width:100px0100px173.2px;border-color:transparenttransparenttransparentred;}便利なジェネレーターもあります。
https://bennettfeely.com/clippy/
currentColorを値として指定すると、現在のcolorプロパティの値が参照されます。
https://zenn.dev/rabee/articles/css-tips-currentcolor
以下のようなボタンの実装例を用意しました。currentColorを使うことで、hover時のsvgの色指定を省略できます。

<aclass="button"href=""><span>BUTTON</span><svg.../> // 矢印アイコン</a>.button{color:white;/* ...略 */}.button:hover{color:blue;/* ...略 */}.button svg{fill: currentColor;/* .buttonのcolorを参照しているので、通常時はwhite、hover時はblueになる */}.button{color:white;/* ...略 */}.button:hover{color:blue;/* ...略 */}.button svg{fill:white;}.button:hover svg{fill:blue;}clamp関数はvwなどの動的な値に対して最大(最小)値を設定できます。
例えば、フォントサイズにvwを指定すると大きく(小さく)なりすぎることがありますが、clamp関数を使うことで最大(最小)の文字サイズを指定できるようになります。ブレイクポイントでvwの値を変えるより直感的に扱えます。
.text{font-size:clamp(16px,5vw,20px);/* ベースサイズは5vw、最小16px、最大20px */}.text{font-size:5vw;}@media(max-width:767px){.text{font-size:8vw;}}便利なジェネレーターもあります。
https://min-max-calculator.9elements.com/
要素の高さを画面いっぱいにするには100vhではなく100svhを指定します。vhはiOSのアドレスバーの高さを含んでしまうので「画面の高さ+アドレスバーの高さ」になってしまいますが、svhはアドレスバーの高さを含まない純粋な「画面の高さ」のみを取得できます。
.main-visual{height:100svh;}https://zenn.dev/tonkotsuboy_com/articles/svh-dvh-lvh-for-all-browser
完全な角丸のボタンを実装する時のborder-radiusには9999pxなどの大きい数値を指定するのではなく100vmaxを指定することで、ボタンがどんな大きさになっても完全な角丸を保てるようになります。

.button{border-radius:100vmax;}.button{border-radius:9999px;}昨今のブラウザではメディア種別のscreenを省略しても「画面」と認識してくれるので、メディアクエリのscreen andは省略しても問題ありません。
@media(min-width:768px){.element{ ...}}@media screenand(min-width:768px){.element{ ...}}また、range記法という記述方法も2023年にリリースされました。
@media(width <=768px){.element{ ...}}https://zenn.dev/tonkotsuboy_com/articles/css-range-syntax
iOS Safariのバージョン16.3以下がサポート外なので、使う場合はコンパイラを挟むことを推奨します。
https://caniuse.com/css-media-range-syntax
スマホやタブレットなどタップで操作をする端末ではhover処理は無効にします。
タップデバイスを判定するにはメディア特性のany-hover: hoverを使います。昨今は小さいノートパソコンや大きいスマホなどがあるので、画面幅で判定するのはよろしくありません。
@media(any-hover: hover){.button:hover{background-color:red;}}@media(min-width:768px){.button:hover{background-color:red;}}https://www.tak-dcxi.com/article/disable-hover-on-mobile-and-hover-implementation-example
メディア特性のprefers-reduced-motionを使うことで、デバイス設定で「視差効果を減らす」が有効かどうかを判定できます。
ユーザーは過度なアニメーションを求めていない場合もあるので、ユーザー側でアニメーションのON/OFFを選択できるように実装してあげることが大切です。
https://www.webcreatorbox.com/tech/prefers-reduced-motion
https://accessible-usable.net/2021/09/entry_210919.html
以下は「視覚効果を減らす」が有効化されている時に、アニメーション時間を極限まで短くする例です。
@media(prefers-reduced-motion: reduce){*,::before,::after{transition-duration:1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;}}Visually Hiddenとは、視覚的には要素を非表示にしたいけど、スクリーンリーダーには読み上げてもらいたい時に使うCSSスニペットです。
.visually-hidden{position: absolute;width:1px;height:1px;padding:0;overflow: hidden;clip:rect(0000);clip-path:inset(50%);white-space: nowrap;border:0;}https://qiita.com/randy39/items/fca820d500dfe9ec1a52
ラジオボタンやチェックボックスのinput要素を非表示にしてスタイリングする際は、display: noneではなくVisually Hiddenを使います。display: noneでinput要素自体を消してしまうとフォーカスが当たらないなどの弊害が生じます。
[type="radio"]{/* visually-hiddenのスタイル */}[type="radio"]{display: none;}親要素の左右にpaddingが指定されている状態で、子要素の幅を画面幅と同じにする場合はcalcとvwを使って実装します。

.wrapper{padding-left:40px;padding-right:40px;}.photo{width:100vw;margin-inline:calc(50%-50vw);}従来の書き方だと、以下のようにpaddingの値に応じて子要素のwidthやmarginの値も変わってしまいます。これだと、レスポンシブ時にpaddingの値が変わったらwidthやmarginも変える必要がありますが、calcとvwを使うことで再定義が不要になります。
.wrapper{padding-left:40px;padding-right:40px;}.photo{width:calc(100vw+80px);/* 80px = 左右のpaddingの合計値 */margin-left:-40px;/* ネガティブマージンで要素を左に移動させる */}このようなレイアウトもcalcとvwを使うことで効率よく実装できます。

.片方だけはみ出させる要素(左配置の場合){width:50vw;margin-left:calc((50vw-50%)*-1);}.片方だけはみ出させる要素(右配置の場合){width:50vw;margin-right:-50vw;}/* 反対側の要素には`width: 50%`を、これらの親要素には`display: flex`を指定します */詳しくはCodepenをご覧ください。
https://codepen.io/dadada-dadada/pen/JjOXqPZ
コンテンツ量が少なくてもフッターを画面最下部に固定するレイアウト手法です。
body{min-height:100dvh;}footer{position: sticky;top:100%;}https://twitter.com/d151005/status/1729690789343527077?s=20
JavaScriptも画像と同様にパフォーマンスに影響を与えやすい項目なので、ファイルの読み込み方やスクロール時の処理の実装方法などをまずは覚えることをおすすめします。
<script>にdefer属性を付けると非同期でJSファイルがダウンロードされます。また、ダウンロード開始をできるだけ早くしたいので</body>の手前ではなく<head>のできるだけ上のほうで読み込ませます。
<head><scriptsrc="script.js"defer> ...</head> ...<scriptsrc="script.js"></body>https://qiita.com/phanect/items/82c85ea4b8f9c373d684
また、type="module"属性を指定することで、そのファイル内で定義した変数はグローバル変数として扱われなくなります。そのため、他のファイルで同じ変数名を使用していても、名前空間が分離されているのでお互いに影響を与えることがなくなります。
<scriptsrc="script.js"defertype="module">https://zenn.dev/kagan/articles/731ca08f45b8c1
JSファイルを<head>の中でdeferを付けて読み込む場合、ページ読み込み時の処理にはDOMContentLoadedイベントを使うことで、DOMツリー構築完了時の実行が保証されます。これにより、要素の取得エラーなどが発生しなくなります。
loadイベントの場合は画像などのリソース読み込み完了後に実行されるので、実行タイミングが遅くなってしまいます。即時実行関数はDOMContentLoadedとほぼ同じタイミングで実行はされますが、もしJSファイルの読み込み位置が変わったら、その位置によって実行タイミングが変わるので実装時に余計な気を遣う必要が出てきます。
window.addEventListener('DOMContentLoaded',()=>{// ここにページ読み込み時の処理を書く});window.addEventListener('load',()=>{// ここにページ読み込み時の処理を書く});(()=>{// ここにページ読み込み時の処理を書く})();スクロールイベントやリサイズイベントは実行される頻度が極端に高いので、ブラウザに負荷がかかり画面がカクカクする原因になります。なので、Debounceという手法で実行頻度を減らしてあげます。
functiondebounce(func, timeout){let timer; timeout= timeout!==undefined? timeout:300;// funcが呼び出されるまでの遅延時間return()=>{const context=this;const args= arguments;clearTimeout(timer); timer=setTimeout(()=>{ func.apply(context, args);}, timeout);};}https://www.freecodecamp.org/news/javascript-debounce-example/
以下はリサイズ時にヘッダーを取得する例です。
constgetHeader=()=>document.querySelector('header');const debouncedFunction=debounce(getHeader)window.addEventListener('resize', debouncedFunction,false);constgetHeader=()=>document.querySelector('header');window.addEventListener('resize', getHeader,false);前項でも書いた通り、スクロールイベントは負荷が高いのであまり使いたくありません。Intersection Observerを使うことで、ブラウザに負荷をかけずにスクロールに応じた処理を実装できます。
https://ics.media/entry/190902/
以下はスクロールアニメーションのサンプルで、data-scroll-anima属性を持つ要素が画面の上下20%の位置までスクロールされたら属性値がtrueになります。
// 監視対象要素const animaElements=document.querySelectorAll("[data-scroll-anima]");// 交差時に実行される関数constdoWhenIntersect=entries=>{const entriesArray=Array.prototype.slice.call(entries,0); entriesArray.forEach((entry)=>{if(entry.isIntersecting){ entry.target.dataset.scrollAnima='true';}});}// IntersectionObserverのオプションconst options={root:null,rootMargin:'-20% 0px -20% 0px',// 要素が画面の上下20%を超えたら監視するthreshold:0};// 対象要素の数だけobserverで監視const observer=newIntersectionObserver(doWhenIntersect, options);animaElements.forEach((box)=>{ observer.observe(box);});[data-scroll-anima]{opacity:0;transition: opacity.3s;}[data-scroll-anima="true"]{opacity:1;}ブレイクポイントに応じて処理を実行する場合、画面幅をリサイズイベントで監視すると前項で書いた通りブラウザに負荷がかかるので、代わりにmatchMediaを使って今のブレイクポイントを判定します。
また、CSS変数にブレイクポイントを指定しておくことで、CSS側でブレイクポイントの値が変わってもJS側での修正は不要になります。
:root{--breackpoint-md:768px;}// ブレイクポイントの値をCSS変数から取得して、matchMediaにセットconst rootStyles=getComputedStyle(document.documentElement);const breackpointMd= rootStyles.getPropertyValue('--breackpoint-md');const mediaQueryList=window.matchMedia(`(max-width:${breackpointMd})`);// ブレイクポイントに応じて実行する処理constmediaQueryFunction=(event)=>{if(event.matches){console.log('768px以下です');}else{console.log('769px以上です');}};// ブレイクポイントが変わった時のイベントを登録mediaQueryList.addEventListener('change', mediaQueryFunction);// ページ読み込み時のイベントを登録window.addEventListener('DOMContentLoaded',()=>mediaQueryFunction(mediaQueryList));https://zenn.dev/no4_dev/articles/878f4afbff6668d4e28a-2
Sassでブレイクポイントの変数を定義している場合、以下のようにCSS変数を登録をすれば上記と同じことができます。
$breackpoint-md:768px;:root{--breackpoint-md:#{$breackpoint-md};}https://twitter.com/hiro_ghap1/status/1550389210317979649
幅320pxのような小さい端末のレスポンシブ対応はCSSで頑張るのではなく、Viewportで表示倍率を縮小します。
昨今のデザインは375pxで作られることが多く、そもそも320px程度まで考慮されていない場合が多いのでCSSで調整するには限界があります。なので、表示倍率を縮小することで実装工数が大幅に削減でき、大量のメディアクエリの記述も発生しなくなります。
constadjustViewport=()=>{const triggerWidth=375;const viewport=document.querySelector('meta[name="viewport"]');const value=window.outerWidth< triggerWidth?`width=${triggerWidth}, target-densitydpi=device-dpi`:'width=device-width, initial-scale=1'; viewport.setAttribute('content', value);}const debouncedFunction=debounce(adjustViewport)// debounce関数は、Debounceの項で解説した関数ですwindow.addEventListener('resize', debouncedFunction,false);target-densitydpi=device-dpiは、数年前にAndroidの4系か6系の一部の端末でViewportが正しく変らない事象が起きて、その対処法として付けていました。
昨今の環境でも必要かどうかはちゃんと調べられていないので、これの有無はご自身で判断してください。正直あってもなくても挙動は変らないと思いますが、あることによって特定の環境でも崩れないのであれば付けたままでもいいと私は思っています。
JavaScriptはES6(ES2015)以降、便利な機能や構文が数多く追加されました。ここからはES6以降に追加されたWeb制作寄りの内容を少し紹介していきます。
https://qiita.com/soarflat/items/b251caf9cb59b72beb9b
テンプレートリテラルを使うことで、変数と文字列の結合が楽になります。
const message=`私は${name}です。`;const message='私は'+ name+'です。';配列に関するメソッドはかなり追加されました。新しい配列を生成するmap、特定の配列を探すfind、配列の有無を確認するsomeなど、これまではfor文で行っていた処理をこれらのメソッドを使うことで記述量が圧倒的に短くなります。
// この中からidが2のデータを検索するconst users=[{id:1,name:'山田'},{id:2,name:'田中'},{id:3,name:'中村'}];const targetUser= users.find(user=> user.id===2);let targetUser;for(let i=0; i< users.length; i++){if(users[i].id===2){ targetUser= users[i];break;}}スプレッド構文を使うことで、配列やオブジェクトの結合や展開が楽になります。
let arr1=[1,2,3];let arr2=[4,5];// 配列の結合let combined=[...arr1,...arr2];// [1, 2, 3, 4, 5]// 配列のコピーlet arrCopy=[...arr1];// [1, 2, 3]// 配列の結合let combined= arr1.concat(arr2);// [1, 2, 3, 4, 5]// 配列のコピーlet arrCopy= arr1.slice();// [1, 2, 3]特定の処理の後に他の処理を実行する場合は Async / await を使います。setTimeoutで遅延させると、必ずしも遅延させた秒数で手前の処理が終わるとは限らないので絶対に辞めましょう。
// データを取得(取得に時間がかかる)asyncfunctionfetchData(){const response=awaitfetch('https://api.example.com/data');const data=await response.json();return data;}asyncfunctionrun(){try{const data=awaitfetchData();// awaitを使うことでfetchData()が完了するまで次の行の処理を待つconsole.log('取得したデータ:', data);}catch(error){console.error('エラーが発生しました:', error.message);}}run();functionfetchData(){// 同様の処理}asyncfunctionrun(){const data=fetchData();// fetchData()の完了を待たずに次の行を実行してしまう// setTimeoutで処理を遅延させているが、fetchData()の完了が2000ミリ秒以内に終わる保証はないため、dataが空の状態でconsole.logが実行される可能性があるsetTimeout(()=>{console.log('取得したデータ:', data);},2000);}run();https://qiita.com/soarflat/items/1a9613e023200bbebcb3
APIなど外部からデータを取得する時はFetchかAxiosを使います。昔はXMLHttpRequestやjQueryのAjaxを使っていましたが、FetchやAxiosのほうが例外処理やデータの扱いに優れています。
fetch('https://api.example.com/data').then(response=> response.json()).then(data=>console.log(data)).catch(error=>console.error(error));const xhr=newXMLHttpRequest();xhr.open('GET','https://api.example.com/data');xhr.onload=function(){if(xhr.status===200){console.log(xhr.responseText);}else{console.error('APIエラー');}};xhr.onerror=function(){console.error('ネットワークエラー');};xhr.send();https://zenn.dev/syu/articles/9840082d1a6633#1.インストール方法
petamorikenさんからコメントをいただいたので追記です。
非同期処理には中断する処理が必要不可欠です。AbortControllerを使い、タイムアウトしたら中断するような処理も同時に実装するようにしましょう。
const controller=newAbortController();const timeoutId=setTimeout(()=> controller.abort(),5000);// 5秒後にAbortSignalを送信fetch('https://api.example.com/data',{signal: controller.signal}).then(response=>{clearTimeout(timeoutId);// タイムアウトをキャンセルreturn response.json();}).then(data=>{console.log('Success:', data);}).catch(err=>{if(err.name==='AbortError'){console.log('Request timed out');}else{console.error('Error:', err);}});https://developer.mozilla.org/ja/docs/Web/API/AbortController
かなりの量を紹介したので一度に全部を使いこなすのは難しいと思います。個人的にこれだけは...をいくつかピックアップしたので、まずはそれだけでも取り入れてみてください。
object-fit、縦横比を制御するにはaspect-ratioを使うgapを使うcalc()とvwを組み合わせてレイアウトを組むDeferで読み込み、処理はDOMContentLoadedイベント内で行うIntersection Observerを使うこちらは普段私が情報をキャッチアップしている方々のサイトです。とても勉強になるので是非訪れてみてください。
https://ics.media/
https://www.tak-dcxi.com/
https://baigie.me/engineerblog/
https://zenn.dev/tonkotsuboy_com
https://zenn.dev/takamoso
https://zenn.dev/yusukehirao
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
網羅的なまとめをありがとうございます。JavaScriptについて一部気になったためコメントさせてください。
deferでJSを読み込む場合、HTMLが全て読み込まれる前にJSが実行されることがあります。
これはasync属性と混同しているように思います。defer属性を付けたスクリプトはHTML文書解析後DOMContentLoadedイベントの直前に実行されます。
https://developer.mozilla.org/ja/docs/Web/HTML/Element/script#defer
また特にモジュール機能を使わなかったとしてもtype="module"とすることで、誤ってグローバル変数を作ってしまうのを防ぐことができますね。おすすめします(外部ファイルとして読み込んだ場合はdefer属性と同じタイミングで実行されます)。
<script>const foo=1;</script><script>console.log(foo);// 1</script><scripttype="module">const foo=1;</script><scripttype="module">console.log(foo);// throws ReferenceError</script>補足になるのですが、Fetchのような非同期処理には中断する処理が不可欠だと思います。今ではWeb標準のAbortController/AbortSignalを使うことができます。
https://developer.mozilla.org/ja/docs/Web/API/AbortController
また最近ではEventTarget.prototype.addEventListenerのオプションにAbortSignalを渡すことで一度に複数のハンドラーを削除することができます(Safari 15から使えます)。
const controller=newAbortController();eventTarget.addEventListener("foo",(e)=>{// signal が abort したらこのハンドラは削除される},{singal: controller.signal});https://blog.jxck.io/entries/2023-06-01/abort-signal-any.html
コメントありがとうございます!後日記事の内容を修正させていただきます。
これはasync属性と混同しているように思います。defer属性を付けたスクリプトはHTML文書解析後DOMContentLoadedイベントの直前に実行されます。
完全にミスっていますね...。修正いたします。
また特にモジュール機能を使わなかったとしてもtype="module"とすることで、誤ってグローバル変数を作ってしまうのを防ぐことができますね。
type="module"はたまに見かけるな〜くらいで全然気にしていなく、この機会にしっかりと理解できたのでよかったです。ありがとうございます。
Fetchのような非同期処理には中断する処理が不可欠だと思います。今ではWeb標準のAbortController/AbortSignalを使うことができます。
AbortControllerも初めて知りました。タイムアウト用などに中断処理は確かに必須ですね。
Lazy loading
ファーストビューに含まれる画像にはつけないことをお勧めします。
LCPやFCPなどのスコアが悪化するだけでなく、実際の体感としても表示速度が遅くなったように感じる場合があります。
重要な画像はprefetchし、そうでない画像は遅延読み込みした方が良いです。
Debounce
例示のコードだと初回(と前回の関数実行から300ms以上経過している状態)の実行時にも300msの遅延が発生します。
イベントで何かしらを描画する場合はアプリケーションが遅く感じる原因になります。
DOMContentLoaded
Good の例はイベントの登録、Bad の例は即時関数なので比較として妥当でないように思いました。
イベントの登録なら load イベントと比較したり、単なる関数なら記述する場所で比較するのはいかがでしょうか。