DMM TV Web速度改善の道 〜Tailwind CSS移行編〜

DMM TV Web速度改善の道 〜Tailwind CSS移行編〜

はじめに

この記事はDMM グループ Advent Calendar 2024の18日目の記事です。

こんにちは。DMM.comプレミアムプロダクト開発部でWebフロントエンジニアをしている渡辺 (@shikachii) です。 普段はWebブラウザ向けDMM TVの開発・保守・運用をしています。(これ以降Webブラウザ向けDMM TVをDMM TV Webと呼びます。)

今回はDMM TV Webで抱えていたCSSに関する課題と、どのように改善しているかについてお話しします。

DMM TV Webで抱えていたCSSの課題

DMM TV WebではCSSライブラリにEmotionを使用しています。 EmotionはJavaScriptでCSSスタイルを記述するために設計されたCSS-in-JSのライブラリです。

私たちのチームでは以下のようにStyled APIを用い開発していました。

// index.tsx
import * as Styled from "./style";

export const Component = () => {
  const onClick = () => console.log("clicked!");
  return (
    <>
      <div>title</div>
      <Styled.Button onClick={onClick}>button</Styled.Button>
    </>
  );
};
// style.tsx
import styled from "@emotion/styled";

export const Button = styled.button`
  padding: 32px;
  background-color: #101010;
  border-radius: 4px;
`;

現状開発をしている中で大きなデメリットは感じていませんでした。 ただ、Emotionは動的スタイリングを実現するためレンダリング時にJavaScriptオブジェクトからCSSへの変換・スタイルの挿入が行われています。 そのため、どうしてもゼロランタイムCSS-in-JSやTailwind CSSなどと比べパフォーマンスに影響を与える可能性がありました。 DMM TV WebはtoCの動画配信サービスであり、常に高いユーザー体験・パフォーマンスを求められます。 現状問題のない範囲ではありましたが、さらなる速度改善ができると考えました。

また、DMM TV WebではNext.jsのPage Routerを使用していますが、次世代のアーキテクチャであるApp Routerへの移行も検討しています。 ですが、App Routerで重要な機能の一つであるRSC(React Server Component) でEmotionはサポート対応中(2024/12/18 現在)となっています。

これらの背景から私たちのチームではEmotionから別のCSSライブラリへ移行することに決めました。

代替ライブラリの比較

移行のためRSCに対応しているCSSライブラリの中から主要なものを抜粋し比較します。 ちょうど同チームの大坂さん (@yud0uhu) が昨年のアドベントカレンダーでCSSライブラリを比較しており、これを参考に候補を挙げることにしました。

2023年のゼロランタイムCSS in JSを考える - DMM inside

ゼロランタイム RSC 記法 スター数
(2024/12/10 現在)
Emotion CSS-in-JS 17543
styled-components CSS-in-JS 40558
CSS Modules CSS 17685 (Docs)
Tailwind CSS Class 83797
Linaria CSS-in-JS 11714
Kuma UI ✅ (ハイブリッド) CSS-in-JS 1799
Panda CSS ✅ (ハイブリッド) CSS-in-JS 5268

観点となっていたゼロランタイム・RSC対応について見ると、CSS Modules、Tailwind CSS、Kuma UI、Linaria、PandaCSSが対応しています。 その中でも GitHubのスター数を見ると Tailwind CSSがもっとも多いです。 単純にスター数が多ければ良いというわけではありませんが、より多くの人の関心を集めておりナレッジが蓄積されていると考えたため、Tailwind CSSを選択しました。

移行のステップ

DMM TV Webは10万行を超える規模のアプリケーションです。 また、汎用性の高い共通コンポーネントをパッケージ化して用いており、合わせると12万行を超えます。 コンポーネント数も1000を超えるため、簡単に移行できるものではありませんでした。

一気に移行・リリースといったフローにすると以下のような課題がありました。

  • 変更する箇所が多くなりすぎて工数や期限を見積もりづらい
  • デグレが発生した際に検証し直しになり手戻りのコストが大きくなる

そのため私たちのチームでは優先度ごとに開発のフェーズを分け、順次リリースするフローにしました。 私たちのチームではアジャイル開発により週に1回リリースを行なっていたため、この方法に親和性がありました。

フェーズ分け

今回の移行では以下の表のようにコンポーネントにそれぞれ優先度を振り分けtierとして書き出して管理しました。

コンポーネント名 tier リリース日
Accordion tier1 2024/08/05
Button tier1 2024/08/05
Carousel tier2 2024/08/12
etc...

これにより、リリースごとに何が変更されるかを管理しやすくなり全体の進捗の動向も掴みやすくなりました。

デグレ発生時の備え

私たちのチームではTailwind CSSへの移行中もユーザー向けの施策やバグ修正を行なっています。 そのため、スタイルによるバグ・デグレが発生した際には安全に手戻りできるよう備えました。

前述したとおり共通コンポーネントライブラリはパッケージ化されています。 これをDMM TV Webに取り込んだ際に不具合が見つかるとバグ修正だけでなくパッケージのPublishを行う必要があり、リードタイムの発生が予想されていました。 そのため、今回は移行前のコンポーネントをコピーし接頭辞にTwとつけて開発しました。 これにより、移行後のコンポーネントにバグ・デグレが発生してもアプリケーション側で接頭辞を外すことで簡単に戻すことができ、互換性を保つことができました。

// button/index.tsx
export const Button = (props: ButtonProps) => {
  return (
    <Styled.Button {...props} />;
  )
};
// tw-button/index.tsx
/** button/index.tsxをコピー */
export const Button = (props: ButtonProps) => {
  return (
    <Styled.Button {...props} />;
  )
};

/** TwButtonにリネームしてスタイルを移行 */
export const TwButton = (props: ButtonProps) => {
  return (
    <button className={buttonWrapper} {...props} />;
  )
}

現在のステータス

現在Tailwind CSSへの移行を進めており、約4割のスタイルが移行完了・リリースされました。

具体的には以下のように、Styled APIで記述されたコンポーネントのスタイルを Tailwind CSSのクラス名に当てはめてclassNameを作っています。 また、動的制御のために clsx、クラス名の競合回避のために tailwind-merge を使用しています。

コード例(Emotion)

/** index.tsx */
import * as Styled from "./style";

export const Component = ({ isExpand }: { isExpand: boolean }) => {
  return (
    <Styled.DivWrapper isExpand={isExpand}>
      any children
    </Styled.DivWrapper>
  );
};
/** style.tsx */
import styled from "@emotion/styled";

export const DivWrapper = styled.div<{ isExpand: boolean }>`
  display: flex;
  justify-content: center;

  ${({ isExpand }) => `
    width: ${isExpand ? "100%" : "50%"};
    margin: ${isExpand ? "16px" : "8px"};
  `}
`;

コード例(Tailwind CSS)

/** ~/utils/tw-merge.ts */
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
/** index.tsx */
import { wrapper } from "./style";

export const Component = ({ isExpand }: { isExpand: boolean }) => {
  return <div className={wrapper(isExpand)}>any children</div>;
};
/** style.tsx */
import { cn } from "~/utils/tw-merge";

// グローバルのfont-sizeを10pxとしているため、m-2(0.8rem)は8pxとなります。
// tailwind-mergeにより"m-2 m-4"となった際は"m-4"のみが出力されます。
export const wrapper = (isExpand: boolean) =>
  cn([
    "flex",
    "justify-center",
    isExpand ? "w-full" : "w-1/2",
    "m-2",
    isExpand && "m-4", // falseとなった場合はclsxにより弾かれます
  ]);

パフォーマンス検証

以下はパフォーマンスの検証としてCore Web Vitalsの数値を移行前と現在で比較した表です。 それぞれ数値が0に近づくほど高パフォーマンスであることを示します。

Desktop LCP(s) INP(s) CLS
移行前 2.08 0.139 0.062
現在 1.99 (-4.32%) 0.131 (-5.76%) 0.060 (-3.23%)
Mobile LCP(s) INP(s) CLS
移行前 2.51 0.236 0.075
現在 2.56 (+1.99%) 0.222 (-6.30%) 0.066 (-12.0%)

移行の間にも他の施策が数多くリリースされているため単純に比較することは難しいですが、4割ほどの移行でもおおむねパフォーマンスの改善が見られました。

さいごに

DMM TV Webでは今回のように課題に対して技術的改善を行なっています。 これからもよりよいサービス・ユーザー体験を提供できるよう、さらなるパフォーマンス改善を目指してApp Routerへの移行など改善を続けていきます。

PR

DMM TVは月額550円(税込)なのに国内作品の見放題数第2位※1の非常識コスパ動画配信サービスです!

アニメ配信数業界最大級で、新作アニメ見放題作品数 No.1※2! さらにアニメ見放題サービスとして2年連続 No.1※3を受賞!

初回登録した方には無料期間もあります! 気になった方はぜひ登録してみてください!

tv.dmm.com

※1 GEM Partners調べ/2024年10月度の実績
動画配信サービスの各社WEBサイトに表示されている邦画、日本ドラマ、日本アニメのSVODコンテンツをカウント (映画は1作品ごと、ドラマ・アニメなどは1話/1エピソードごとに1件とカウント)

※2 国内定額制動画配信サービスで配信された2023年公開の新作アニメ作品が対象。 調査期間2023年12月〜2023年12月22日。(株)コミュニケーション科学研究所調べ。

※3 国内最大級の商品比較サービス「マイベスト」調べ。受賞時期:2023年9月度、2024年9月度。