React でのレンダリング動作を理解する

公開: 2020-11-16

生、死、運命、税金と並んで、React のレンダリング動作は、人生における最大の真実と謎の 1 つです。

飛び込みましょう!

他のみんなと同じように、私は jQuery を使ってフロントエンド開発の旅を始めました。 当時、純粋な JS ベースの DOM 操作は悪夢でした。 その後、ゆっくりと、JavaScript ベースのフレームワークが非常に目立つようになり、もはやそれらを無視できなくなりました。

最初に学んだのは Vue でした。 コンポーネントや状態、その他すべてがまったく新しいメンタル モデルだったので、信じられないほど苦労しました。すべてをはめ込むのは大変でした。 おめでとう、相棒、私は自分に言い聞かせました。 これで、残りのフレームワークを学習する必要が生じた場合でも、非常に簡単になります。

ある日、React を学び始めたとき、自分がいかに間違っていたかに気づきました。 Facebook は、フックを投入して、「これからはこれを使ってください。 ただし、クラスを書き直さないでください。 クラスは大丈夫です。 実際、それほど多くはありませんが、大丈夫です。 しかし、フックはすべてであり、未来です。

とった? 偉大な!"。

やがて、私もあの山を越えました。 しかしその後、 React自体と同じくらい重要かつ困難な問題にぶつかりました。

サプライズ!!!

React でのレンダリングとその謎に出くわしたことがあるなら、私が話していることを知っているでしょう。 そうでない場合は、何が用意されているかわかりません。

しかし、何かに時間を浪費する前に、それから何が得られるかを尋ねるのは良い習慣です (私とは異なり、興奮しすぎて、それのためだけに何でも喜んで学びます)。 このレンダリングが何であるかを気にせずに React 開発者としての生活が順調に進んでいるなら、気にする必要はありません。 良い質問です。最初にこれに答えてみましょう。次に、レンダリングとは何かを見ていきます。

React でのレンダリング動作を理解することが重要なのはなぜですか?

React の学習は、JSX と呼ばれるものを返す (最近では機能的な) コンポーネントを作成することから始めます。 また、この JSX がページに表示される実際の HTML DOM 要素に変換されることも理解しています。 状態が更新されるとページが更新され、ルートが予想どおりに変更され、すべて問題ありません。 しかし、React がどのように機能するかについてのこの見方は単純であり、多くの問題の原因です。

完全な React ベースのアプリを作成することに成功することがよくありますが、アプリケーションの特定の部分 (またはアプリケーション全体) が著しく遅いことに気付くことがあります。 そして最悪の部分。 . . 理由はまったくわかりません。 すべてを正しく実行し、エラーや警告は表示されず、コンポーネント設計、コーディング標準などのすべての優れたプラクティスに従っており、ネットワークの速度低下や高価なビジネス ロジックの計算が舞台裏で行われることはありません。

まったく別の問題である場合もあります。パフォーマンスに問題はないのに、アプリの動作がおかしくなることがあります。 たとえば、認証バックエンドに対して 3 つの API 呼び出しを行いますが、他のすべてに対しては 1 つだけです。 または、一部のページが 2 回再描画され、同じページの 2 つのレンダリング間の目に見える遷移が不快な UX を生み出しています。

大野! 二度とない!!

さらに悪いことに、このような場合に利用できる外部のヘルプはありません。 お気に入りの開発フォーラムに行ってこの質問をすると、彼らは次のように答えます。 ここに最低限の実用的な例を添付できますか?」 もちろん、法的な理由からアプリ全体を添付することはできませんが、実際のアプリのようにシステム全体とやり取りしていないため、その部分の小さな動作例にはその問題が含まれていない可能性があります。

ねじ込み? ええ、あなたが私に尋ねれば。

ですから、そのような悲惨な日々を見たくないのであれば、理解を深めることをお勧めします—そして興味を持ってください、私は主張しなければなりません。 しぶしぶ得た理解は、React の世界、つまり React でのレンダリングと呼ばれるこのよく理解されていないもので、あなたを遠くまで連れて行くことはありません。 信じてください。理解するのはそれほど難しくありません。マスターするのは非常に困難ですが、隅々まで知らなくても、本当に遠くまで行くことができます。

Reactでレンダリングとはどういう意味ですか?

それは素晴らしい質問です。 私たちは React を学ぶときにそれを尋ねることはあまりありません (私はそれを知らなかったので知っています)。 辞書の意味はまったく異なりますが (この議論では重要ではありません)、私たちプログラマーは、それが何を意味するかについて既に理解しています。 画面、3D API、グラフィックス カードを操作し、製品仕様を読むことで、「レンダリング」という言葉を読んだときに「絵を描く」という行に沿って何かを考えるように心を訓練します。 ゲーム エンジン プログラミングでは、レンダラーがあり、その唯一の仕事は、まさに、シーンから渡された世界をペイントすることです。

そのため、React が何かを「レンダリング」すると、すべてのコンポーネントが収集され、Web ページの DOM が再描画されると考えられます。 しかし、React の世界では (もちろん、公式ドキュメントでも)、レンダリングとはそうではありません。 それでは、シートベルトを締めて、React の内部を深く掘り下げてみましょう。

「私はのろわれます。 . 」

React は仮想 DOM と呼ばれるものを維持し、それを実際の DOM と定期的に比較し、必要に応じて変更を適用することを聞いたことがあるでしょう (これが、jQuery と React を一緒に投入できない理由です — React が完全に制御する必要があります) DOM)。 現在、この仮想 DOM は、実際の DOM のように HTML 要素で構成されているのではなく、React 要素で構成されています。 違いは何ですか? 良い質問! 小さな React アプリを作成して、自分の目で確かめてみませんか?

この目的のために、この非常にシンプルな React アプリを作成しました。 コード全体は、数行を含む単一のファイルです。

 import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }

私たちがここで何をしているのか注目してください。

はい、単純に JSX 要素がどのように見えるかをログに記録します。 これらの JSX 式とコンポーネントは、私たちが何百回も書いてきたものですが、何が起こっているのかについてほとんど注意を払っていません。 ブラウザーの開発コンソールを開いてこのアプリを実行すると、次のように展開されるObjectが表示されます。

これは威圧的に見えるかもしれませんが、いくつかの興味深い詳細に注意してください。

  • 私たちが見ているのは、プレーンで通常の JavaScript オブジェクトであり、DOM ノードではありません。
  • プロパティpropsには、 AppclassName (コードで設定された CSS クラス) があり、この要素には 2 つの子があることが示されていることに注意してください (これも一致し、子要素は<h1> > タグと<h2>タグです)。 .
  • _sourceプロパティは、ソース コードが要素の本体を開始する場所を示します。 ご覧のとおり、ファイルApp.jsをソースとして指定し、行番号 6 に言及しています。コードをもう一度見ると、行 6 が JSX タグの開始タグの直後にあることがわかります。これは理にかなっています。 JSX の括弧には React 要素が含まれています。 それらは後でReact.createElement()呼び出しに変換するのに役立つため、その一部ではありません。
  • __proto__プロパティは、このオブジェクトがそのすべてを派生させていることを示しています。 ルート JavaScript Objectからのプロパティであり、ここで見ている日常的な JavaScript オブジェクトにすぎないという考えを再び強化します。

これで、いわゆる仮想 DOM は実際の DOM のようには見えず、その時点での UI を表す React (JavaScript) オブジェクトのツリーであることがわかりました。

*はぁ* 。 . . 私たちはまだそこにいますか?

疲れ果てた?

私を信じてください、私もそうです。 これらのアイデアを頭の中で何度も何度も繰り返して、可能な限り最良の方法で提示しようとし、それらを引き出すための言葉を考えて、それらを再配置することは簡単ではありません.

しかし、気が散ってしまいます!

ここまで生き残った私たちは、求めていた質問に答えることができる立場になりました: React でのレンダリングとは何ですか?

レンダリングとは、仮想 DOM をウォークスルーし、現在の状態、小道具、構造、UI で必要な変更などを収集する React エンジン プロセスです。React は、いくつかの計算を使用して仮想 DOM を更新し、新しい結果を実際の DOM と比較します。ページで。 この計算と比較は、React チームが公式に「調整」と呼んでいるものです。彼らのアイデアや関連するアルゴリズムに興味がある場合は、公式ドキュメントを確認してください。

コミットする時が来ました!

レンダリング部分が完了すると、React は「commit」と呼ばれるフェーズを開始し、その間に必要な変更を DOM に適用します。 これらの変更は同期的に適用され (次々に適用されますが、同時に機能する新しいモードがすぐに期待されます)、DOM が更新されます。 React がこれらの変更を正確にいつ、どのように適用するかは私たちの関心事ではありません。なぜなら、それは完全に内部にあるものであり、React チームが新しいことを試すにつれて変化し続ける可能性が高いからです。

React アプリでのレンダリングとパフォーマンス

ここまでで、レンダリングとは情報を収集することであり、毎回視覚的な DOM の変更を行う必要がないことを理解しました。 また、「レンダリング」とは、レンダリングとコミットを含む 2 段階のプロセスであることもわかっています。 React アプリでレンダリング (さらに重要な再レンダリング) がどのようにトリガーされるか、詳細を知らないとアプリのパフォーマンスが低下する可能性があることを確認します。

親コンポーネントの変更による再レンダリング

React の親コンポーネントが変更された場合 (たとえば、その状態または props が変更されたため)、React はツリー全体をこの親要素の下に移動し、すべてのコンポーネントを再レンダリングします。 アプリケーションに多くのネストされたコンポーネントと多くの相互作用がある場合、親コンポーネントを変更するたびに、無意識のうちにパフォーマンスが大幅に低下します (変更したいのは親コンポーネントのみであると仮定します)。

確かに、レンダリングによって React が実際の DOM を変更することはありません。これは、調整中に、これらのコンポーネントが何も変更されていないことが検出されるためです。 しかし、それは依然として CPU 時間とメモリの浪費であり、その合計の速さに驚かれることでしょう。

コンテキストの変更による再レンダリング

React の Context 機能は、誰もが気に入っている状態管理ツールのようです (これはまったく目的として作成されていません)。 これはとても便利です。最上位のコンポーネントをコンテキスト プロバイダーでラップするだけで、あとは簡単です。 React アプリの大部分はこのように構築されていますが、この記事をこれまで読んだことがあれば、おそらく何が問題なのかを発見したことでしょう。 はい、コンテキスト オブジェクトが更新されるたびに、すべてのツリー コンポーネントの大規模な再レンダリングがトリガーされます。

ほとんどのアプリはパフォーマンスを認識していないため、誰も気付かないのですが、前述のように、大量の高インタラクション アプリでは、このような見落としが非常に高くつく可能性があります。

React レンダリング パフォーマンスの向上

では、これらすべてを考慮して、アプリのパフォーマンスを改善するにはどうすればよいでしょうか? できることがいくつかあることがわかりましたが、機能コンポーネントのコンテキストでのみ説明することに注意してください。 クラスベースのコンポーネントは、React チームによって強く推奨されておらず、廃止されつつあります。

状態管理に Redux または同様のライブラリを使用する

Context の簡単で汚い世界が好きな人は、Redux を嫌う傾向がありますが、これには正当な理由があって非常に人気があります。 これらの理由の 1 つはパフォーマンスです。Redux のconnect()関数は、必要に応じてそれらのコンポーネントのみを (ほとんどの場合) 正しくレンダリングする魔法のようなものです。 はい、標準の Redux アーキテクチャに従うだけで、パフォーマンスは無料になります。 Redux アーキテクチャを採用すれば、ほとんどのパフォーマンス (およびその他) の問題をすぐに回避できると言っても過言ではありません。

memo()を使用してコンポーネントを「フリーズ」する

「メモ」という名前は、キャッシングの派手な名前であるメモ化に由来します。 キャッシングをあまり経験したことがなくても大丈夫です。 ここに簡略化された説明があります。計算/演算の結果が必要になるたびに、以前の結果を維持していた場所を調べます。 見つかった場合は、その結果を返すだけです。 そうでない場合は、先に進んでその操作/計算を実行します。

memo()に飛び込む前に、まず React で不要なレンダリングがどのように発生するかを見てみましょう。 簡単なシナリオから始めます。ユーザーがサービス/製品を気に入った回数を示すアプリ UI の小さな部分 (ユース ケースを受け入れるのに問題がある場合は、Medium で「拍手」できる方法を考えてください) 」を複数回使用して、記事をどれだけサポート/気に入っているかを示します)。

いいねを 1 ずつ増やすことができるボタンもあります。最後に、ユーザーに基本的なアカウントの詳細を表示する別のコンポーネントがあります。 これを理解するのが難しい場合でも、まったく心配する必要はありません。 ここで、すべてのステップバイステップのコードを提供します (そして、それほど多くはありません)。最後に、動作中のアプリをいじって理解を深めることができるプレイグラウンドへのリンクを提供します。

まず、顧客情報に関するコンポーネントに取り組みましょう。 次のコードを含むCustomerInfo.jsというファイルを作成しましょう。

 import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };

派手なことはありませんよね?

ユーザーがアプリを操作しても変化しないと予想される情報テキスト (小道具を介して渡された可能性があります)アプリケーションの、それは実質的に静的です)。 ただし、 console.log()ステートメントに注意してください。 これは、コンポーネントがレンダリングされたことを知る手がかりになります (「レンダリングされた」とは、その情報が収集され、計算/比較されたことを意味し、実際の DOM に描画されたことを意味するわけではありません)。

したがって、テスト中にブラウザー コンソールにそのようなメッセージが表示されない場合、コンポーネントはまったくレンダリングされていません。 10 回表示されている場合は、コンポーネントが 10 回レンダリングされたことを意味します。 等々。

それでは、メイン コンポーネントがこの顧客情報コンポーネントをどのように使用するかを見てみましょう。

 import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }

したがって、 Appコンポーネントには、 useState()フックによって管理される内部状態があることがわかります。 この状態は、ユーザーがサービス/サイトを気に入った回数をカウントし続け、最初はゼロに設定されています。 React アプリに関する限り、難しいことはありませんよね? UI 側では、次のようになります。

少なくとも私には、このボタンは壊されたくないほど魅力的に見えます! ただし、その前に、ブラウザーの開発コンソールを開いてクリアします。 その後、ボタンを数回押してみると、次のように表示されます。

ボタンを 19 回押しましたが、予想どおり、いいね! の合計数は 19 でした。配色が非常に読みづらかったので、赤いボックスを追加して主要なものを強調表示しました: <CustomerInfo />コンポーネント20回レンダリングされました!

なんで20?

すべてが最初にレンダリングされたときに 1 回、次にボタンが押されたときに 19 回。 ボタンは、 <App />コンポーネント内の状態の一部であるtotalLikesを変更し、その結果、メイン コンポーネントが再レンダリングされます。 また、この投稿の前のセクションで学んだように、その中のすべてのコンポーネントも再レンダリングされます。 <CustomerInfo />コンポーネントはプロセスで変更されず、レンダリング プロセスに寄与したため、これは望ましくありません。

どうすればそれを防ぐことができますか?

このセクションのタイトルが示すとおり、 memo()関数を使用して、 <CustomerInfo />コンポーネントの「保存」またはキャッシュされたコピーを作成します。 メモ化されたコンポーネントを使用すると、React はその props を調べて以前の props と比較し、変更がない場合、React はこのコンポーネントから新しい「レンダリング」出力を抽出しません。

次のコード行をCustomerInfo.jsファイルに追加しましょう。

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

うん、それは私たちがする必要があるすべてです! これをメイン コンポーネントで使用して、何かが変わるかどうかを確認します。

 import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }

はい、変更されたのは 2 行だけですが、とにかくコンポーネント全体を表示したかったのです。 UI に関しては何も変わっていないので、新しいバージョンを試して、いいねボタンを数回押すと、次のようになります。

では、コンソール メッセージはいくつあるでしょうか。

唯一! これは、最初のレンダリングを除いて、コンポーネントがまったく変更されなかったことを意味します。 非常に大規模なアプリでのパフォーマンスの向上を想像してみてください! わかりました、わかりました、約束したコードプレイグラウンドへのリンクはここにあります。 前の例を複製するには、 CustomerInfo.jsからMemoizedCustomerInfoの代わりにCustomerInfoをインポートして使用する必要があります。

とは言っても、 memo()はどこにでもまき散らして魔法のような結果を期待できる魔法の砂ではありません。 memo()を過度に使用すると、アプリにトリッキーなバグが発生する可能性があり、場合によっては、予想される更新が失敗することがあります。 ここでも、「時期尚早」の最適化に関する一般的なアドバイスが適用されます。 まず、直感でアプリを構築します。 次に、集中的なプロファイリングを行って、どの部分が遅いかを確認し、メモ化されたコンポーネントが適切なソリューションであると思われる場合にのみ、これを導入します。

「インテリジェント」なコンポーネント設計

私が「インテリジェント」を引用符で囲んだ理由は次のとおりです。1) インテリジェンスは非常に主観的で状況に左右されます。 2) 知的な行動はしばしば不快な結果をもたらす。 したがって、このセクションに対する私のアドバイスは、自分がしていることに自信を持ちすぎないことです。

レンダリング パフォーマンスを向上させる 1 つの可能性は、コンポーネントの設計と配置を少し変えることです。 たとえば、再レンダリングを回避するために、子コンポーネントをリファクタリングして階層の上位に移動できます。 「ChatPhotoView コンポーネントは常に Chat コンポーネント内になければならない」というルールはありません。 特殊なケース (パフォーマンスが影響を受けているというデータに裏打ちされた証拠があるケース) では、ルールを曲げたり破ったりすることは実際には素晴らしいアイデアです。

結論

一般的に、React アプリを最適化するためにできることは他にもたくさんありますが、この記事はレンダリングに関するものであるため、議論の範囲を制限しました。 とにかく、React の内部で何が起こっているのか、実際のレンダリングとは何か、そしてそれがアプリケーションのパフォーマンスにどのように影響するのかについて、より良い洞察が得られたことを願っています。

次に、React Hooks とは何かを理解しましょう。