一. 特徴
Fragment
テンプレートが fragment と string 型をサポートし、ReactElementの配列や文字列を返せるようになりました。
v16.2.0では、JSXのFragmentサポート として <></> も提供されました。
Error Boundary
コンポーネントレベルのエラー処理です。子コンポーネントツリー内部の例外をキャッチできるようになり、UI層のフォールバック策となります。
Portal
コンポーネントツリーとDOMツリーの構造が一致しないことを許容します。ホバーカードやツールチップなどのシナリオで利用されます。
例えば、ツールチップはDOM構造上、ターゲットとチップが(レイアウトの都合で)兄弟関係になることが一般的ですが、論理的にはチップはターゲットに属する親子関係です。Portal機能はこのようなシナリオを処理するために使用されます。
特筆すべき点として、イベントバブリングが処理されており、Portalコンポーネントの親コンポーネントは依然としてバブリングの通知を受け取ることができます(React 16以前からDOMイベントバブリングの差異を解消するためのイベントシステムが内蔵されていましたが、ここでは「迂回バブリング」もサポートされています サンプル)。
カスタムDOM属性のサポート
以前はHTML/SVG属性名のホワイトリストが内蔵されており、カスタム属性はインターセプトされて無視されていましたが、React 16ではこの制限が撤廃されました。
この制限を撤廃したのには2つの理由があります。1つ目は、内蔵された属性フィルタリングが非標準な(例えばプロポーザル段階の)新属性や他のライブラリ/フレームワーク(AngularやPolymerなど)に対して不親切であったこと。2つ目は、バンドル内に決して小さくない属性ホワイトリストを持たせる必要があり、そのメンテナンスが非常に手間であったことです。
サーバーサイドレンダリング(SSR)の改善
React 15より3倍速い(ベンチマーク時。実際の業務シナリオでは1.3倍程度とのこと)と謳われており、いくつかの改善が行われました:
-
ストリーム(Stream)のサポート
-
ビルド時に不要な
process.envへのアクセス(Node環境でこの変数にアクセスするのは時間がかかります)を削除 -
クライアント側でチェックサムを計算せず、可能な限り既存のDOMを再利用(DOMノードの再利用を徹底しているInfernoに似ていますが、Infernoの再利用はいくつかの問題に直面したようで、現在は目玉機能ではなくオプションとして提供されています)
注意:React 16でもDOMノードの再利用に関するいくつかの問題が存在するようです:
However, it’s dangerous to have missing nodes on the server render as this might cause sibling nodes to be created with incorrect attributes.
P.S. 具体的にどのような危険なシナリオに注意すべきかは、公式が後日専用のブログ記事を出す可能性があり、現時点では明確に説明されていません。
ファイルサイズの削減
Reactバンドルの軽量化(リファクタリング、フラットなパッケージング戦略、Rollupへの移行)により、サイズが30%削減されました。
二. SSR
最も変化が大きかったのはSSRでしょう。今回は真面目に実装されています(以前のSSRはついでに作られたようなものでした)。
1. 新しいAPI
サーバー側で、renderToString と renderToStaticMarkup にそれぞれ対応する renderToNodeStream と renderToStaticNodeStream が追加されました。
クライアント側では hydrate が追加されました。
2. 緩やかな整合性チェック
クライアント側のチェックがそれほど厳格ではなくなりました:
-
React 15では、クライアントは受け取ったSSR結果に対して文字レベルで整合性チェックを行い、少しでも不一致があればクライアント側で再生成して全体を置き換えていました。
-
React 16では属性の順序が異なっていても許容されるようになり、不一致な属性を自動修正することもなくなりました。また、タグ構造が一致しない場合でも、全体を置き換えるのではなくサブツリー単位で修正を行います。
さらに、サーバー側で生成されるHTML構造からチェックサム(data-react-checksum)やID(data-reactid)が削除され、レスポンスサイズがかなり小さくなります:
<!-- react 15 -->
<div data-reactroot="" data-reactid="1"
data-react-checksum="122239856">
<!-- react-text: 2 -->This is some <!-- /react-text -->
<span data-reactid="3">server-generated</span>
<!-- react-text: 4--> <!-- /react-text -->
<span data-reactid="5">HTML.</span>
</div>
<!-- react 16 -->
<div data-reactroot="">
This is some <span>server-generated</span> <span>HTML.</span>
</div>
3. パフォーマンスの最適化
デフォルトで不要な process.env.NODE_ENV へのアクセスが削除され、手動でコンパイルして削除する必要がなくなりました。
SSRで一時的な仮想DOMを作成しなくなったため、全体的にかなり高速化されました。
ストリーム(Stream)のサポートによるパフォーマンス上のメリットは以下の通りです:
-
サーバー側で生成しながら送信できるため、SSRの完了を待たずに送信を開始でき、TTFB(Time To First Byte)が向上します。
-
クライアント側で受信しながら描画できるため、レスポンス内容が完全に揃うのを待たずに描画を開始でき、解析、描画、外部リソースの読み込みなどのタイミングが早まります。
4. Error BoundaryとPortalは非サポート
React 16のSSRでは、Error Boundary と Portal はサポートされていません。
サーバー側で子コンポーネントのレンダリングでエラーが発生しても、Error Boundaryで食い止めることはできませ��。ストリームのパフォーマンス上のメリットのために、Error Boundaryは犠牲にされました:
This is intentional / a known limitation. With streaming rendering it's impossible to "call back" markup that has already been sent, and we opted to keep renderToString and renderToNodeStream's output identical.
renderToNodeStream や renderToStaticNodeStream だけでなく、renderToString も Error Boundary をサポートしていません。上述の通り、出力を一致させるために、2つの仕組みを維持しないことにしたためです。
P.S. SSRにおけるError Boundaryの詳細については、componentDidCatch doesn't work in React 16's renderToString を参照してください。
また、Portal機能は「逆流」を引き起こす可能性があり、Error Boundaryと同様の理由でストリームメカニズム下ではサポートできません(既に送信済みのストリームにPortalの内容を挿入することは、当然不可能です)。
三. Fiber
全く新しいコアアーキテクチャです。(2年を費やして)コンポーネントのレンダリングメカニズムを全面的に書き直しました。最も重要な機能は非同期レンダリング(Async Rendering)であり、スケジューリング可能なレンダリングを実現しました(マウントプロセスがいったん始まると中断できないという問題を根本的に解決しました)。
リファクタリングの過程
これほど巨大な、文字通り骨身を削るようなリファクタリングのプロセスは非常に興味深いものです。簡単にまとめると:
-
新しいブランチを切らない。代わりに
useFiberという機能フラグで切り替える手法を取りました。これは日常的なメンテナンスや競合処理を簡素化するためだそうです。 -
まず骨組み(スケルトン)を作る。一部のAPIをサポートし、徐々にすべてのテストケース(いわゆるTDD:テスト駆動開発で、最終的に2000件のケースを作成)をパスさせていきました。
-
エンジニアリングによる補助手段。進捗追跡、単体テスト結果の追跡(コミットによって何が修正され、何が壊れたかを特定しやすくするため。手法は非常にシンプルで、
tests-failing.txtとtests-passing.txtをGitの追跡対象に含めました)、継続的な本番環境での検証(いわゆる ドッグフーディング。初期段階から全テストがパスするまで、絶えず「実戦」での検証を繰り返しました。これは一種の「目に見える信念」と言えるでしょう)。 -
適切な業務サービス をテストベッド(試験場)として選ぶ。ある程度安定した後、実際のサービスを通じて本番環境への準備が整ったことを証明しました。A/Bテストでデータを取り、2億人のユーザーに体感してもらい、その後全ユーザーに展開。同時に社内システムも全面的に移行して検証シナリオを拡大し、最後にReact Nativeアプリで段階的にリリース(カナリアリリース)を行いました。
-
新しいメカニズムを直接導入しない。当面は依然として同期方式で実行し(Fiberは非同期をサポートしていますが)、スムーズな移行のためにさらに数ヶ月間寝かせる準備をしています。
P.S. 具体的なリファクタリングの過程については、React 16: A look inside an API-compatible rewrite of our frontend UI library を参照してください。
そのため、現時点では非同期レンダリングをサポートしていません:
This initial React 16.0 release is mostly focused on compatibility with existing apps. It does not enable asynchronous rendering yet. We will introduce an opt-in to the async mode later during React 16.x. We don't expect React 16.0 to make your apps significantly faster or slower, but we'd love to know if you see improvements or regressions.
メリット
-
新機能
コンポーネントレベルのエラー処理や、
renderで複数のコンポーネントを返すといった、以前は実現が難しかった機能が、リファクタリングによって実現可能になりました。 -
ユーザー体験上のメリット
Fiberは必ずしも速くなるとは限りませんが、よりスムーズになります(レンダリングタスクを分割してバランスよくスケジューリングすることで、メインスレッドの長時間占有を避けます)。また、タスクの優先度制御により、アニメーションなどを優先的に実行させることができます。
差異はかなり明確です。React Stack vs Fiber を参照してください。
コメントはまだありません