🐡

『レンダリングとマウント』をJavaScript、React、Nextjsで比較

に公開

下記の記事はClaudeに質問して出力してもらった内容を、自分で理解しながら追記/修正したものになります。


(Claudeによる出力)
↓↓↓↓ここから↓↓↓↓

はじめに

Reactを学び始めると、「レンダリング」や「マウント」といった用語に出会います。これらは似ているようで異なる概念であり、しっかり理解することでより効率的なReactアプリケーションが作れるようになります。

この記事では、JavaScriptの基本から始めて、React、そしてNext.jsでの「レンダリング」と「マウント」の違いを初心者にもわかりやすく解説します。

目次

  1. 基本概念:レンダリングとマウントとは?
  2. JavaScriptでの要素表示の仕組み
  3. Reactにおけるレンダリングとマウント
  4. Next.jsの特徴とレンダリング戦略
  5. 実践:コーディング時に意識すべきポイント
  6. よくある問題とその解決法
  7. まとめ

基本概念:レンダリングとマウントとは?

レンダリングとは

レンダリングとは、簡単に言えば「画面に表示するための準備をする過程」です。

  • JavaScriptでは: HTML要素を作成してDOMに追加する作業
  • Reactでは: コンポーネントの状態から仮想DOMを計算する作業
  • Next.jsでは: サーバーまたはビルド時にHTMLを生成する作業

マウントとは

マウントとは、「準備したものを実際に画面に表示する過程」です。

  • JavaScriptでは: 明確な「マウント」という概念はなく、DOM要素の追加と同義
  • Reactでは: 仮想DOMを実際のDOMに反映させる作業
  • Next.jsでは: サーバーレンダリングされたHTMLをクライアント側で「ハイドレーション」する作業

(追記)用語解説

■仮想DOM・・・JavaScript上で実際のDOM(Document Object Model)を抽象化した軽量なコピーを指します。React や Vue.js などのフレームワークで使われ、効率的なUI更新を実現するための仕組み。

<仮想DOMを使うメリット>

  • パフォーマンスの向上
    直接DOMを操作すると遅くなるが、仮想DOMを使うことで最小限の更新で済む。
  • コードのシンプル化
    DOM操作を直接書く必要がなくなり、状態管理が簡単になる。
  • バグを減らせる
    直接DOMを触るより安全で、意図しない更新を防げる。

■ハイドレーション・・・サーバーサイドレンダリング(SSR)で生成されたHTMLに、クライアント側でReactのイベントや状態を復元し、インタラクティブにするプロセス のこと。(仕組みについて、後述があります)


<ハイドレーションのメリット>
✅ 高速な初回表示
 → SSRによって最初のHTMLがすぐに描画される。

✅ SEOに強い
 → HTMLが事前に生成されるため、Googleのクローラーが認識しやすい。

✅ クライアント側の負荷を軽減
 → 必要な部分のみReactで管理することで、クライアントの処理負担が減る。


JavaScriptでの要素表示の仕組み

JavaScriptでは、DOM API を使って要素を作成し、表示します。

// 新しい要素を作成
const newElement = document.createElement('div');
newElement.textContent = 'こんにちは!';

// 作成した要素をDOMに追加(これで画面に表示される)
document.getElementById('container').appendChild(newElement);

注意点:

  • DOM操作は比較的重い処理
  • 大量のDOM操作はパフォーマンスに影響する

初心者向けTips:

  • 大量の要素を追加する場合は、DocumentFragmentを使うとパフォーマンスが向上します
  • JavaScriptファイルの読み込みはレンダリングをブロックするので、deferasync属性の使用を検討しましょう

Reactにおけるレンダリングとマウント

Reactでは、「レンダリング」と「マウント」が明確に分かれています。

Reactのレンダリングプロセス

  1. コンポーネントが評価される(関数コンポーネントが実行される)
  2. JSXが仮想DOM(Virtual DOM)に変換される
  3. 前回の仮想DOMと比較される(差分検出)
function Greeting({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

// このコンポーネントが評価されるとレンダリングが行われる

Reactのマウントプロセス

  1. 差分が検出されたら、実際のDOMを更新する
  2. 更新されたDOMが画面に反映される
// 初回マウント
ReactDOM.render(<App />, document.getElementById('root'));

// React 18以降
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

重要なライフサイクル

コンポーネントには、以下のような「ライフサイクル」があります:

  1. マウント時:コンポーネントが初めてDOMに追加される
  2. 更新時:propsや状態が変わってコンポーネントが再レンダリングされる
  3. アンマウント時:コンポーネントがDOMから削除される
import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    console.log('コンポーネントがマウントされました');
    
    return () => {
      console.log('コンポーネントがアンマウントされます');
    };
  }, []); // 空の依存配列でマウント時のみ実行
  
  return <div>Hello React</div>;
}

初心者向けTips:

  • useEffectの依存配列が空の場合、マウント時のみ実行されます
  • DOM操作や外部APIとの通信はuseEffect内で行いましょう
  • レンダリング中に副作用(APIコールなど)を起こさないようにしましょう

Next.jsの特徴とレンダリング戦略

Next.jsは、Reactをベースに、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)を簡単に実装できるフレームワークです。

Next.jsのレンダリング方式

  1. サーバーサイドレンダリング (SSR)

    • サーバーでHTMLを生成し、クライアントに送信
    • ユーザーごとに異なるコンテンツに最適
    // pages/ssr-page.js
    export async function getServerSideProps() {
      const res = await fetch('https://api.example.com/data');
      const data = await res.json();
      return { props: { data } };
    }
    
    function SSRPage({ data }) {
      return <div>{data.title}</div>;
    }
    
  2. 静的サイト生成 (SSG)

    • ビルド時にHTMLを生成
    • ブログ記事など、頻繁に変更されないコンテンツに最適
    // pages/ssg-page.js
    export async function getStaticProps() {
      const res = await fetch('https://api.example.com/data');
      const data = await res.json();
      return { props: { data } };
    }
    
    function SSGPage({ data }) {
      return <div>{data.title}</div>;
    }
    
  3. クライアントサイドレンダリング (CSR)

    • 通常のReactと同じように、クライアント側でレンダリング
    // pages/csr-page.js
    import { useEffect, useState } from 'react';
    
    function CSRPage() {
      const [data, setData] = useState(null);
      
      useEffect(() => {
        async function fetchData() {
          const res = await fetch('https://api.example.com/data');
          const data = await res.json();
          setData(data);
        }
        fetchData();
      }, []);
      
      if (!data) return <div>Loading...</div>;
      return <div>{data.title}</div>;
    }
    

ハイドレーション

Next.jsでは、サーバーからHTMLを受け取った後、Reactがそのマークアップを「ハイドレーション」します。これは、静的なHTMLにイベントリスナーなどの機能を付与するプロセスです。

ハイドレーションのステップ:

  1. サーバーから生成されたHTMLを受け取る
  2. JavaScriptが読み込まれる
  3. Reactがサーバーで生成されたDOMと一致するように仮想DOMを構築
  4. イベントリスナーを追加し、インタラクティブにする

初心者向けTips:

  • ハイドレーションエラーを避けるため、サーバーとクライアントで生成されるHTMLが一致するようにする
  • windowdocumentなどのブラウザAPI使用時には注意が必要(サーバーでは使えない)

実践:コーディング時に意識すべきポイント

レンダリングの最適化

  1. 不要な再レンダリングを防ぐ

    // コンポーネントのメモ化
    const MemoizedComponent = React.memo(MyComponent);
    
    // 計算値のメモ化
    const calculatedValue = useMemo(() => expensiveCalculation(a, b), [a, b]);
    
    // コールバック関数のメモ化
    const handleClick = useCallback(() => {
      console.log('クリックされました');
    }, []);
    
  2. 状態の更新を最小限に

    // 悪い例
    const [user, setUser] = useState({ name: 'Taro', age: 25 });
    
    // 一部だけ更新したい場合でも全体を更新してしまう
    setUser({ name: 'Jiro', age: 25 });
    
    // 良い例
    setUser(prevUser => ({ ...prevUser, name: 'Jiro' }));
    

マウント時の処理

  1. DOMの参照を取得する

    import { useRef, useEffect } from 'react';
    
    function MyComponent() {
      const myElementRef = useRef(null);
      
      useEffect(() => {
        // マウント後にDOMノードにアクセス可能
        if (myElementRef.current) {
          myElementRef.current.focus();
        }
      }, []);
      
      return <input ref={myElementRef} />;
    }
    
  2. クリーンアップ関数の重要性

    useEffect(() => {
      const subscription = someAPI.subscribe();
      
      // クリーンアップ関数(アンマウント時に実行される)
      return () => {
        subscription.unsubscribe();
      };
    }, []);
    

Next.jsでのデータ取得タイミング

// SSRでのデータ取得
export async function getServerSideProps() {
  // リクエストごとに実行される
  return { props: { /* データ */ } };
}

// SSGでのデータ取得
export async function getStaticProps() {
  // ビルド時のみ実行される
  return { props: { /* データ */ } };
}

// クライアントサイドでのデータ取得
function ClientComponent() {
  const { data, error } = useSWR('/api/data', fetcher);
  // ...
}

よくある問題とその解決法

1. 無限ループに陥るuseEffect

// 問題のあるコード
function BadComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // 状態が更新されるとuseEffectが再実行される
  }); // 依存配列がない!
  
  return <div>{count}</div>;
}

// 解決策
function GoodComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 初回のみ実行
    setCount(prevCount => prevCount + 1);
  }, []); // 空の依存配列
  
  return <div>{count}</div>;
}

2. ハイドレーションエラー

// 問題のあるコード
function DateComponent() {
  // サーバーとクライアントで異なる値になる
  return <div>{new Date().toLocaleString()}</div>;
}

// 解決策
function SafeDateComponent() {
  const [date, setDate] = useState(null);
  
  useEffect(() => {
    setDate(new Date().toLocaleString());
  }, []);
  
  return <div>{date || 'Loading...'}</div>;
}

3. パフォーマンス問題

// 問題のあるコード
function ExpensiveComponent({ data }) {
  // データが変わるたびに高コストな計算
  const processedData = expensiveOperation(data);
  
  return <div>{processedData}</div>;
}

// 解決策
function OptimizedComponent({ data }) {
  // データが変わった場合のみ計算
  const processedData = useMemo(() => {
    return expensiveOperation(data);
  }, [data]);
  
  return <div>{processedData}</div>;
}

まとめ

Reactと Next.js でアプリケーションを開発する際、レンダリングとマウントの違いを理解することは非常に重要です。

覚えておくべきポイント

  1. レンダリング はUIを計算する過程、マウント は計算したUIを実際のDOMに反映する過程
  2. useEffect の依存配列を正しく設定し、無限ループを避ける
  3. サーバーサイドとクライアントサイドの違いを理解し、ハイドレーションエラーを防ぐ
  4. メモ化(React.memo, useMemo, useCallback)を活用して最適化する
  5. 適切なレンダリング戦略(SSR, SSG, CSR)を選択する

Reactでの開発経験を積みながら、これらの概念をより深く理解していきましょう。最初は難しく感じるかもしれませんが、少しずつ実践していくことで、効率的で高パフォーマンスなアプリケーションを構築できるようになります。

参考資料


(Claudeによる出力)
↑↑↑↑ここまで↑↑↑↑

さいごに

以上がClaudeに解説してもらった内容になります。

ブログサイトを作成する際の技術スタックとしてNextjsを選択したのですが、あまりCSR/SSGとSSRの使い分けができておらず無駄な部分は結構ありそう・・・。かといって直すにはまだ知識不足な気がします。継続して学習していきたいと思います。

GitHubで編集を提案

Discussion