Advanced
Kyle Mo
Become a better React developer
React Concepts - (2)
State Of React
- React Renderer
- Streaming SSR
- Suspense For Data Fetching
- Concurrent Features
- Server Components
- React Compiler (AST)
- Maybe Not React
React Renderer
Virtual DOM is overhead ?
- Diffing
- DOM Manipulation
const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;
const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.querySelector("input").value = "bar";
Virtual DOM 透過 Diffing 找出最佳方式,再進行最佳化 DOM 操作
如果本身就會寫效能好的 DOM 操作 ->
理論上效能會比 Virtual DOM 好,因為 Virtual DOM 多做了 Diffing,且記憶體用量比較大(需要 maintain 一個樹狀結構)
Virtual DOM is, by definition, slower than carefully crafted manual updates.
Virtual DOM (Fiber Tree) 的目的從來就不是效能,而是提升 Developer Experience:開發者不用管 DOM 怎麼更新、抽象化、元件化、Composition、任務優先級、相對其他框架寫法上更能運用 JavaScript 的能力...等等。
Svelte 沒有採用 Virtual DOM,它靠在編譯階段得知之後會有哪些
DOM 操作,提前產生對應的程式碼,可以產生差不多甚至超越 Virtaul DOM 的效能。
所以 Virtual DOM 不好嗎?不見得,把 Diffing 邏輯抽出來,與平台解耦,讓跨平台成為可能。
React
React Native
React Three Fiber
React Reconcilier + ReactDOM
React Reconcilier + Native Renderer
React Reconcilier + Three.js Renderer
How to write a custom Renderer ?
Streaming SSR
Original SSR
-
在 server side 抓取想要的資料
-
Server render 出 HTML
-
頁面的 HTML, CSS, JS 被送到 client side
-
使用者這時候可以看到畫面,但還不能進行互動
-
React 進行 hydration,賦予 UI 互動能力
Hydration
Problems
-
You have to fetch everything before you can show anything
-
You have to load everything before you can hydrate anything
-
You have to hydrate everything before you can interact with anything
😨
Streaming HTML In React
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Note: for this to work, your data fetching solution needs to integrate with Suspense
Initial HTML
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
Data Ready
Solve What Problem ?
You have to fetch everything before you can show anything
Selective Hydration In React 18
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Solve What Problem ?
You have to load everything before you can hydrate anything
Interacting with the page before all the components have hydrated
Even..
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Solve What Problem ?
You have to hydrate everything before you can interact with anything
TLDR
-
You no longer have to wait for all the data to load on the server before sending HTML. Instead, you start sending HTML as soon as you have enough to show a shell of the app, and stream the rest of the HTML as it’s ready.
-
You no longer have to wait for all JavaScript to load to start hydrating. Instead, you can use code splitting together with server rendering. The server HTML will be preserved, and React will hydrate it when the associated code loads.
-
You no longer have to wait for all components to hydrate to start interacting with the page. Instead, you can rely on Selective Hydration to prioritize the components the user is interacting with, and hydrate them early.
And they are all relative about fiber architecture we talked about last time...
Suspense For Data Fetching
Fetch On Render
useEffect(() => {
fetchSomething().then(result => setState(result));
}, []);
理論上效率、體驗最差,Render 後才去呼叫 API,一層一層的 Render,造成抓資料時的 Waterfall
Fetch Then Render
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
// Kick off fetching as early as possible
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}
// The child doesn't trigger fetching anymore
function ProfileTimeline({ posts }) {
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Using Relay or GraphQL
Render as You Fetch (Suspense)
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
在渲染前儘早的開始抓資料,並立刻的開始渲染下一個 Chunk,這時資料若處於未 Ready 的狀態,會 throw Promise 並進入 Suspense 的狀態,等到 Promise Resolve 後,React 會進行 Retry(這時候資料已經 Ready 了)
Solve Race Condition In Effect
useEffect(() => {
fetchUser(id).then(u => setUser(u));
}, [id]);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
dispatch(requestStarted());
try {
fetch(url, { signal: abortController.signal });
// code omitted for brevity
dispatch(requestSuccessful({ data }));
} catch (e) {
dispatch(requestFailed({ error: e.message }));
}
};
fetchData();
return () => {
abortController.abort();
};
}, [url]);
Concurrent Features
Concurrent Features
Concurrent Mode
One is Suspense...and another is
Transition
React Transitions are (blocks of) state updates, that the developer instructs React to deprioritize and delay if something more important comes along.
CSS Transition
Visual States
startTransition For Low-priority updates
Pending State
And they are all relative about fiber architecture we talked about last time...
Server Components
Server Components (.server.js(/ts/jsx/tsx))
Client Components (.client.js(/ts/jsx/tsx))
Share Components (.js(/ts/jsx/tsx))
Types Of Components
Hybrid Rendering
Server Component
Client Component
How The Hybrid Rendering Work ?
- Render Root
- Request For Server Components
- React Runtime Rendering
function Root() {
const [data, setData] = useState({});
// 向伺服器發送請求
const componentResponse = useServerResponse(data);
return (
<DataContext.Provider value={[data, setData]}>
componentResponse.render();
</DataContext.Provider>
);
}
Request For Server Components
Server Component Root
Server
Component 1
Client
Component 1
Server
Component 2
Client
Component 2
Request For Server Components
M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
M4:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":["$","ul",null,{"className":"notes-list","children":[["$","li","0",{"children":["$","@4",null,{"id":0,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","1",{"children":["$","@4",null,{"id":1,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","2",{"children":["$","@4",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","3",{"children":["$","@4",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}]]}]}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! ð¥º"}]}]}]}]]}]
module.exports = {
tag: 'Server Root',
props: {...},
children: [
{ tag: "Client Component1", props: {...}: children: [] },
{ tag: "Server Component1", props: {...}: children: [
{ tag: "Server Component2", props: {...}: children: [] },
{ tag: "Server Component3", props: {...}: children: [] },
]}
]
}
React Runtime Rendering
React Runtime
M1
Client
J0
Server
Render
Request JS Bundle
Response Text (Streaming)
Browser
Client Root
Server
App
Server
Component 1
Client
Component 1
React Runtime
Render
Response Text
(Streaming)
Advantages Of React Server Components
Reduce Bundle Size
Server Side Capabilities
Automatic Code Splitting
import ClientComponent1 from './ClientComponent1';
function ServerComponent() {
return (
<div>
// Client Component 會自動被 Code Splitting
<ClientComponent1 />
</div>
)
}
Server Components VS SSR
SSR Navigation
RSC Navigation
Suspense For Data Fetching ?
Server Components
React Compiler
React Forget
How to build a compiler ?
AST (Abstract Syntax Tree)
AST(抽象語法樹)是一種樹狀結構,用於表示程式碼的語法結構。當編譯器或解釋器讀取程式碼時,它首先將其解析成這樹狀結構,以便於後續的分析和處理。每個樹的節點都代表了程式碼中的一個結構元素,如變量、運算符或函數。利用AST,工具和技術可以更容易地進行程式碼轉換、優化和分析。
What Can AST Do ?
AST 長什麼樣子?
使用範例
為了兼容老舊瀏覽器,我們通常會使用 webpack 打包編譯我們的程式碼,將 ES6 語法降低版本,例如箭頭函數變成普通函數。將 const、let 聲明改成 var 等等,這都是透過 AST 來完成的,只不過實現的過程比較複雜,精緻。不過主要包含以下三步驟:
- JS 語法解析成 AST
- 修改 AST
- AST 轉成目標語法( ex: JS, Machine Code )
為什麼要理解 AST ?
雖然日常開發應用比較少會碰到 AST,但學會 AST 是打開開發各種工具機會的敲門磚,也讓我們有機會參與更底層工具的開發與研究。
(EX: Parser, Compiler, Bundler)
Maybe Not React ?
也許 React Forget 也救不了 React ?
Subscription reactivity 訂閱式響應 vs. Comparison reactivity 比較式響應
function A() {
const [text, setText] = useState(''');
return <B text={text} />
}
function B({text}) {
return <C text={text} />;
}
// ...
function Z({text}) {
return <input value={text} />;
}
Imagine This...
無法做到 Fine-Grained Reactivity
比較式響應的問題
function Todo({todos, showDone, text}) {
const filteredTodos = todos.map(todo => todo.done === showDone);
return (
<div>
<ul>{filteredTodos.map(todo => <li>{todo.task}</li>)}<ul>
<input value={text} />
</div>
)
}
即便加上 memoize,還是要做比較
(剛剛那個例子換成 context 也一樣)
什麼情況下可能會有問題?
-
在大型或有 Deeply Nested 元素的網頁
-
有長列表或表格
-
使用者輸入高頻率,如 text input 文字輸入、slider / range input 拖動輸入
Try These Frameworks
- Qwik
- Preact (signals)
- Svelte (Compiler)
- Solid
- Vue 3 (Proxy Based Reactivity)
Conclusion
- Know future progress of React
- Know the limits of React
- Know the design philosophy of React
- Know the differences between React & other frameworks
- Don't think React is always the only solution
React - 02
By kylemo860617
React - 02
- 217