React官方團隊推出了最新的 React Server Components 技術(shù)
那些常見的渲染模式
CSR
客戶端渲染(Client Side Rendering) 應該是前端最熟悉的一種模式了。從前端的發(fā)展歷程上看,富客戶端目前也已經(jīng)成為現(xiàn)代前端應用的主流架構(gòu)。從jQuery到React, Vue框架,基本上所有的交互邏輯都在瀏覽器上得以實現(xiàn)。瀏覽器會先獲取HTML文檔、再走完JS、CSS等資源的加載,最后獲取數(shù)據(jù)渲染到頁面上,這一整個流程走完在時間上相對來說就比較長。所以,后面衍生出了很多按需加載、預加載等技術(shù)來優(yōu)化前端頁面的性能。

靜態(tài)渲染
靜態(tài)渲染發(fā)生在構(gòu)建階段(build time), 通常應用于博客、新聞頁等有著大量靜態(tài)內(nèi)容的站點。由于不必動態(tài)生成頁面,可以提前將頁面靜態(tài)渲染好后,部署在多個CDN上,從而加快響應速度。如果你用過Hexo等個人博客生成工具,就應該比較熟悉這種模式。需要提前將頁面中所有的URL渲染成對應的HTML,再將這些HTML資源部署到靜態(tài)服務器上。使用靜態(tài)渲染的站點由于不需要執(zhí)行很多的js代碼,所以響應速度十分迅速,對于大訪問量的博客、新聞等站點十分合適。在React 生態(tài)中, Gatsby 在靜態(tài)渲染這塊就完成得很出色。
SSR
服務端渲染(SSR),也是業(yè)界常說的頁面直出,通過直接在服務端上將頁面渲染成靜態(tài)HTML,然后分發(fā)給客戶端從而提高首屏加載速度。但是服務器渲染頁面也是需要時間的,雖然能提高TTI(time to interactive), 但也會延長TTFB(Time to First Byte)的時間。通常我們會將SSR和CSR進行結(jié)合使用,SSR可以用在landing page等需要首屏快速響應的頁面上,而后續(xù)的交互應用可以使用CSR。在這個領域,成熟的Next.js成為了很多公司的首選。

預渲染( Prerendering)
采用純客戶端渲染的問題是瀏覽器需要在本地先進行復雜的運算后,頁面才能開始工作。為了解決這個問題,后來又有了預渲染的技術(shù)。它同SSR十分類似,也是通過讓服務端在構(gòu)建階段預先生成靜態(tài)的HTML、CSS和JS文件,然后在客戶端通過注水(hydrated)的方式獲得可交互的頁面。從技術(shù)實現(xiàn)上,可以借助PhantomJS這種無頭瀏覽器來提前渲染好頁面。然后讓代理服務器將用戶請求指向這些預渲染的頁面。如果需要應對SEO的需要,也可以借助 https://prerender.io 這種在線服務,可以提前將動態(tài)頁面保存起來提供給爬蟲使用。在服務端使用中間件,一旦檢測到是來自爬蟲的請求,就讓其訪問預渲染的頁面版本。比起SSR來說,使用客戶端預渲染的好處就是不需要借助Node.js服務器,就可以實現(xiàn)較快的首屏渲染速度。但是由于需要提前編譯頁面,因此頁面一旦有更新,就得調(diào)用預渲染方法。另外,還需要處理路由信息,來告訴代理服務器哪些路由需要使用預渲染的頁面。
流式渲染
在實踐過程中,我們通常會將SSR和CSR結(jié)合使用,但是純粹的SSR在首屏渲染性能上還是可能存在缺陷。為了進一步減少TTFB的時間,還可以采用流式渲染的策略,通過將頁面分割成小的chunk的形式來發(fā)送HTML,瀏覽器就可以更快時間地接受到響應,并逐塊開始渲染。在React中, 可以借助 renderToNodeStream 這個API來進行處理。
import?{?renderToNodeStream?}?from?'react-dom/server';
import?MyApp?from?'./MyApp';
const?header?=?"My?App ";
const?root?=?"";
const?footer?=?"";
app.get('/',?(req,?res)?=>?{
??res.write(header);
??res.write(root);
??const?stream?=?renderToNodeStream(<MyApp?/>);
??stream.pipe(res,?{end:?false});
??stream.on('end',?()?=>?{
????res.write(footer);
????res.end()
??})
});
RSC
什么是React Server Components
這是一個React即將推出的一個新特性,可以讓我們只在服務器端就可以渲染組件。在官方提供的 演講 的開頭,Dan為我們介紹了React框架主要試圖解決的三個限制,一個是良好的用戶體驗,第二個是極低的維護成本,第三個是更好的性能。這三個約束在以往的實踐過程當中,我們通常都只能被迫選擇其中的兩個。所以為了同時得到這三個好處,React Server Components這項技術(shù)就應運而生了。那么它要怎么在這三大方面發(fā)力呢? 從用戶體驗上來看,服務器組件同之前介紹的使用SSR不同的是,每次需要更新的時候,瀏覽器都會請求服務器,然后服務器會將更新后的組件以流(Stream)的形式下發(fā)給瀏覽器,從而可以獲得漸進式渲染的能力。同時由于下發(fā)的是已經(jīng)渲染好的中間狀態(tài)的數(shù)據(jù)格式,也不會丟失客戶端組件的狀態(tài)。從遷移成本上來說,你可以將原有的應用以部分或者全部的形式無縫切換到服務器組件上,而無需大量重寫原有的代碼。最后一方面就是性能了,也是它的主打賣點。利用服務器組件可以極大地減少最終應用打包后的體積。開發(fā)過程中,我們常常需要引入第三方庫,而這些第三方庫都會無形中增加最終打包代碼的體積。而通過服務器組件,這些第三方庫的代碼并不會包含在最終的bundle.js文件中。瀏覽器也不會下載任何服務器組件的js代碼,這也包括了它們的依賴性。這樣對于那些在服務器當中使用大量依賴項的組件來說,這是一個特別好的策略。因為這代表著服務器下發(fā)給客戶端的,僅僅只是經(jīng)過渲染后的元數(shù)據(jù)。瀏覽器拿到這些元數(shù)據(jù),直接渲染到頁面上就可以了。由于打包體積的減小,順其自然的,應用的性能勢必會得到提升。同時,服務器組件還可以讓我們直接訪問服務器的資源,這也比直接通過C/S模式來訪問資源有著更好的性能。
工作原理
RSC的工作流程,本質(zhì)上和傳統(tǒng)的C/S模式是比較類似的,都是客戶端發(fā)起請求,然后服務器匹配路由,并根據(jù)對應的參數(shù)(在這里指RSC的Props)來渲染對應的組件。服務器會將渲染后的組件以元數(shù)據(jù)的形式下發(fā)到客戶端中去。以官方提供的例子為例,最終服務器下發(fā)的數(shù)據(jù)就是以序列化后的JSON形式存在(該協(xié)議在后續(xù)官方可能會進行修改)。
M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col?sidebar","children":?[...]}}]]
其中,J開頭的指代Server組件實體,就是在Server執(zhí)行React.createElement(Server組件)的JSON序列化結(jié)果。而M開頭的組件則代表了客戶端組件,其中的數(shù)據(jù)是webpack打包后的引用路徑,客戶端拿到這些引用信息后直接可以拿到對應的客戶端模塊,接著直接在本地渲染即可。而S表示Suspense組件,用來處理應用的過渡狀態(tài),可以理解為程序中的占位符。E是Error組件,處理錯誤。瀏覽器拿到這些序列化后的數(shù)據(jù)后,就可以開始渲染對應的組件了。React框架層在后續(xù)客戶端更新渲染過程(reconciliation)中,也可以選擇直接跳過服務器組件,從而大大提高性能。

接著,在后續(xù)應用更新過程中,瀏覽器需要觸發(fā)服務器組件更新的時候,會重新構(gòu)造請求,獲取對應的序列化結(jié)果,而這些序列化結(jié)果會同現(xiàn)有的UI樹進行合并渲染,從而觸發(fā)更新。同時,舊的瀏覽器狀態(tài)也不會丟失。

日常使用
根據(jù)官方提供的Demo, 目前客戶端組件和服務端組件主要是通過命名規(guī)范來區(qū)分(client.js代表客戶端組件,server.js代表服務器組件),借助Webpack打包工具來完成客戶端組件代碼的打包。然后借助Node.js服務器,來渲染服務器組件。而對于很多只用來處理轉(zhuǎn)換數(shù)據(jù)的組件來說,并不需要額外的狀態(tài)或者說副作用。那么這些組件無論是在客戶端還是服務端運行,本質(zhì)是一樣的,于是我們就可以將它們提取為共享組件(Shared Components)。并且這些共享組件可以根據(jù)引用它的位置來決定自身角色。如果是在服務端組件中被引用,那么它的行為就跟服務端組件一致,如果是在客戶端組件當中被引用,那么它的行為就與客戶端組件保持一致。

服務端方面,需要額外提供一個api, 用來下發(fā)渲染后的組件元數(shù)據(jù):

主要源碼解析
服務端
Node.js服務端這邊主要是需要提供一個/react接口,客戶端獲取對應的服務器組件的時候,利用這個接口來獲取對應的元數(shù)據(jù)信息:
app.get('/react',?function(req,?res)?{
??sendResponse(req,?res,?null);
});
//?處理參數(shù)、并根據(jù)參數(shù)來渲染對應的組件
function?sendResponse(req,?res,?redirectToId)?{
??//?參數(shù)處理
??const?location?=?JSON.parse(req.query.location);
??if?(redirectToId)?{
????location.selectedId?=?redirectToId;
??}
??//?該值后續(xù)會用來做緩存的key
??res.set('X-Location',?JSON.stringify(location));
??//?渲染react組件,并發(fā)送回瀏覽器
??renderReactTree(res,?{
????selectedId:?location.selectedId,
????isEditing:?location.isEditing,
????searchText:?location.searchText,
??});
}
async?function?renderReactTree(res,?props)?{
??//?獲取客戶端webpack打包模塊信息
??await?waitForWebpack();
??const?manifest?=?readFileSync(
????path.resolve(__dirname,?'../build/react-client-manifest.json'),
????'utf8'
??);
??const?moduleMap?=?JSON.parse(manifest);
??pipeToNodeWritable(React.createElement(ReactApp,?props),?res,?moduleMap);
}
服務器在處理的時候,會附帶上客戶端webpack打包模塊的信息,主要是因為服務器組件的子組件中可能也會包含客戶端組件,此時就需要客戶端模塊信息來構(gòu)造返回值。pipeToNodeWritable這個方法是服務器組件邏輯處理的入口函數(shù):
function?pipeToNodeWritable(model,?destination,?webpackMap)?{
??//?參數(shù)為根元素,response,?客戶端webpack模塊信息
??var?request?=?createRequest(model,?destination,?webpackMap);
??//?綁定'drain'回調(diào),?此處用于做限流處理
??destination.on('drain',?createDrainHandler(destination,?request));
??//?開始調(diào)度
??startWork(request);
}
首先會先生成request對象用來后續(xù)做處理:
function?createRequest(model,?destination,?bundlerConfig)?{
??//?創(chuàng)建基礎的request對象,會從根節(jié)點開始解析
??//?pingedSegments數(shù)組用來存儲需要處理的節(jié)點
??var?pingedSegments?=?[];
??var?request?=?{
????destination:?destination,
????bundlerConfig:?bundlerConfig,
????cache:?new?Map(),
????nextChunkId:?0,
????pendingChunks:?0,
????pingedSegments:?pingedSegments,
????completedModuleChunks:?[],
????completedJSONChunks:?[],
????completedErrorChunks:?[],
????writtenSymbols:?new?Map(),
????writtenModules:?new?Map(),
????flowing:?false,
????toJSON:?function?(key,?value)?{
??????//?序列化方法,后面會介紹
??????return?resolveModelToJSON(request,?this,?key,?value);
????}
??};
??
??
??request.pendingChunks++;
??var?rootSegment?=?createSegment(request,?function?()?{
????return?model;
??});
??pingedSegments.push(rootSegment);
??
??
??return?request;
}
核心原理是處理每一個子組件,然后利用scheduleWork方法來調(diào)度運行。
//?每個組件在處理過程中會被當做是一個segment
function?createSegment(request,?query)?{
??var?id?=?request.nextChunkId++;
??var?segment?=?{
????id:?id,
????query:?query,
????ping:?function?()?{
??????return?pingSegment(request,?segment);
????}
??};
??return?segment;
}
function?pingSegment(request,?segment)?{
??var?pingedSegments?=?request.pingedSegments;
??pingedSegments.push(segment);
??if?(pingedSegments.length?===?1)?{
????scheduleWork(function?()?{
??????return?performWork(request);
????});
??}
}
function?performWork(request)?{
??var?prevDispatcher?=?ReactCurrentDispatcher.current;
??var?prevCache?=?currentCache;
??ReactCurrentDispatcher.current?=?Dispatcher;
??currentCache?=?request.cache;
??var?pingedSegments?=?request.pingedSegments;
??request.pingedSegments?=?[];
??for?(var?i?=?0;?i?????var?segment?=?pingedSegments[i];
????retrySegment(request,?segment);
??}
??if?(request.flowing)?{
????flushCompletedChunks(request);
??}
??ReactCurrentDispatcher.current?=?prevDispatcher;
??currentCache?=?prevCache;
}
在調(diào)度過程中,進行節(jié)點解析。此時可以看到,會根據(jù)React DOM樹上的節(jié)點信息來判斷組件類型,并生成對應的元數(shù)據(jù),最終返回給瀏覽器。
function?retrySegment(request,?segment)?{
??//?開始解析組件
??var?query?=?segment.query;
??var?value;
??try?{
????value?=?query();
????while?(typeof?value?===?'object'?&&?value?!==?null?&&?value.$$typeof?===?REACT_ELEMENT_TYPE)?{
??????var?element?=?value;?
??????segment.query?=?function?()?{
????????return?value;
??????};
??????//?根據(jù)組件類型嘗試渲染
??????//?返回的數(shù)據(jù)結(jié)構(gòu)類似于:??[REACT_ELEMENT_TYPE,?type,?key,?props]
??????value?=?attemptResolveElement(element.type,?element.key,?element.ref,?element.props);
????}
????//?最后將處理完后的json中間數(shù)據(jù)結(jié)構(gòu)保存起來,最后會返回給瀏覽器
????var?processedChunk?=?processModelChunk(request,?segment.id,?value);
????request.completedJSONChunks.push(processedChunk);
??}?catch?(x)?{
????if?(typeof?x?===?'object'?&&?x?!==?null?&&?typeof?x.then?===?'function')?{
??????//?先暫時掛起,后續(xù)再嘗試重新渲染
??????var?ping?=?segment.ping;
??????x.then(ping,?ping);
??????return;
????}?else?{
???????//?發(fā)送序列化過的錯誤組件chunk
???????emitErrorChunk(request,?segment.id,?x);
????}
??}
}
function?attemptResolveElement(type,?key,?ref,?props)?{
??//?...
??//?構(gòu)造最終返回的數(shù)據(jù)結(jié)構(gòu)
??if?(typeof?type?===?'function')?{
????//?This?is?a?server-side?component.
????return?type(props);
??}?else?if?(typeof?type?===?'string')?{
????//?原生節(jié)點,如html,?div等
????return?[REACT_ELEMENT_TYPE,?type,?key,?props];
??}?else?if?(typeof?type?===?'symbol')?{
????if?(type?===?REACT_FRAGMENT_TYPE)?{
??????return?props.children;
????}
????return?[REACT_ELEMENT_TYPE,?type,?key,?props];
??}?else?if?(type?!=?null?&&?typeof?type?===?'object')?{
????//?客戶端組件,type中會記錄對應的文件路徑等信息
????if?(isModuleReference(type))?{
??????return?[REACT_ELEMENT_TYPE,?type,?key,?props];
????}
????switch?(type.$$typeof)?{
??????case?REACT_FORWARD_REF_TYPE:
????????{
??????????var?render?=?type.render;
??????????return?render(props,?undefined);
????????}
??????case?REACT_MEMO_TYPE:
????????{
??????????return?attemptResolveElement(type.type,?key,?ref,?props);
????????}
????}
??}
function?processModelChunk(request,?id,?model)?{
??//?序列化服務器組件的model?chunk
??var?json?=?stringify(model,?request.toJSON);
??var?row?=?serializeRowHeader('J',?id)?+?json?+?'\n';
??return?convertStringToBuffer(row);
}
function?processModuleChunk(request,?id,?moduleMetaData)?{
??//?序列化客戶端組件的chunk
??var?json?=?stringify(moduleMetaData);
??var?row?=?serializeRowHeader('M',?id)?+?json?+?'\n';
??return?convertStringToBuffer(row);
}
來看一下具體的序列化過程:
function?resolveModelToJSON(request,?parent,?key,?value)?{
??//....
????//?服務器組件
??switch?(value)?{
????case?REACT_ELEMENT_TYPE:
??????return?'$';
??}?
??while?(typeof?value?===?'object'?&&?value?!==?null?&&?value.$$typeof?===?REACT_ELEMENT_TYPE)?{
????var?element?=?value;
????try?{
??????//?嘗試直接渲染服務器組件,遞歸處理
??????value?=?attemptResolveElement(element.type,?element.key,?element.ref,?element.props);
????}?catch?(x)?{
??????if?(typeof?x?===?'object'?&&?x?!==?null?&&?typeof?x.then?===?'function')?{
????????//?掛起處理,后續(xù)再渲染
????????request.pendingChunks++;
????????var?newSegment?=?createSegment(request,?function?()?{
??????????return?value;
????????});
????????var?ping?=?newSegment.ping;
????????x.then(ping,?ping);
????????return?serializeByRefID(newSegment.id);
??????}?else?{
????????request.pendingChunks++;
????????var?errorId?=?request.nextChunkId++;
????????emitErrorChunk(request,?errorId,?x);
????????return?serializeByRefID(errorId);
??????}
????}
??}
??if?(value?===?null)?{
????return?null;
??}
??if?(typeof?value?===?'object')?{
????if?(isModuleReference(value))?{
??????//?客戶端組件
??????var?moduleReference?=?value;
??????var?moduleKey?=?getModuleKey(moduleReference);
??????var?writtenModules?=?request.writtenModules;
??????var?existingId?=?writtenModules.get(moduleKey);
??????if?(existingId?!==?undefined)?{
????????if?(parent[0]?===?REACT_ELEMENT_TYPE?&&?key?===?'1')?{
??????????return?serializeByRefID(existingId);
????????}
????????return?serializeByValueID(existingId);
??????}
??????try?{
????????//?直接從webpack的文件中獲取對應的元數(shù)據(jù)信息
????????var?moduleMetaData?=?resolveModuleMetaData(request.bundlerConfig,?moduleReference);
????????request.pendingChunks++;
????????var?moduleId?=?request.nextChunkId++;
????????emitModuleChunk(request,?moduleId,?moduleMetaData);
????????writtenModules.set(moduleKey,?moduleId);
????????if?(parent[0]?===?REACT_ELEMENT_TYPE?&&?key?===?'1')?{
??????????return?serializeByRefID(moduleId);
????????}
????????return?serializeByValueID(moduleId);
??????}?catch?(x)?{
????????request.pendingChunks++;
????????var?_errorId?=?request.nextChunkId++;
????????emitErrorChunk(request,?_errorId,?x);
????????return?serializeByValueID(_errorId);
??????}
????}
??}
??//?其他組件類型
??if?(typeof?value?===?'string')?{
????return?escapeStringValue(value);
??}
??if?(typeof?value?===?'boolean'?||?typeof?value?===?'number'?||?typeof?value?===?'undefined')?{
????return?value;
??}
??//...
??if?(typeof?value?===?'symbol')?{
????var?writtenSymbols?=?request.writtenSymbols;
????var?_existingId?=?writtenSymbols.get(value);
????if?(_existingId?!==?undefined)?{
??????return?serializeByValueID(_existingId);
????}
????var?name?=?value.description;
}
做序列化,生成最終要返回給瀏覽器的JSON元數(shù)據(jù):
流式渲染的處理邏輯
在閱讀源碼的過程中,可以注意到,RSC使用了流式渲染的處理邏輯。此處,主要將組件數(shù)據(jù)以chunk的形式下發(fā)給瀏覽器,從而提高頁面性能:
function?pipeToNodeWritable(model,?destination,?webpackMap)?{
??//?攜帶了節(jié)點信息,response,?客戶端webpack模塊信息
??var?request?=?createRequest(model,?destination,?webpackMap);
??//?綁定'drain'回調(diào),?此處用于做限流處理
??destination.on('drain',?createDrainHandler(destination,?request));
??//?開始調(diào)度
??startWork(request);
}
//?大數(shù)據(jù)量的時候,當寫入數(shù)據(jù)超出緩存區(qū)閾值時,會用到drain事件,
//?drain的回調(diào)用來繼續(xù)將數(shù)據(jù)返回給客戶端
function?createDrainHandler(destination,?request)?{
??return?function?()?{
????return?startFlowing(request);
??};
}
function?startFlowing(request)?{
??request.flowing?=?true;
??//?將處理完成的chunk分發(fā)到客戶端
??flushCompletedChunks(request);
}
//?用一個flag來標志該函數(shù)是否已經(jīng)在調(diào)用過程中了
var?reentrant?=?false;
function?flushCompletedChunks(request)?{
??//?如果已經(jīng)在處理中的話就返回
??if?(reentrant)?{
????return;
??}
??reentrant?=?true;
??var?destination?=?request.destination;
??beginWriting(destination);
??try?{
????//?先處理客戶端組件的chunk,這樣可以盡快渲染
????var?moduleChunks?=?request.completedModuleChunks;
????var?i?=?0;
????//?模塊分發(fā)出去
????for?(;?i???????request.pendingChunks--;
??????var?chunk?=?moduleChunks[i];
??????//?數(shù)據(jù)返回
??????if?(!writeChunk(destination,?chunk))?{
????????request.flowing?=?false;
????????i++;
????????break;
??????}
????}
????//?將已處理的模塊清掉
????moduleChunks.splice(0,?i);
????//?處理服務端組件的chunk
????var?jsonChunks?=?request.completedJSONChunks;
????i?=?0;
????for?(;?i???????request.pendingChunks--;
??????var?_chunk?=?jsonChunks[i];
??????if?(!writeChunk(destination,?_chunk))?{
????????request.flowing?=?false;
????????i++;
????????break;
??????}
????}
????jsonChunks.splice(0,?i);
????//?最后再處理錯誤的chunk
????var?errorChunks?=?request.completedErrorChunks;
????i?=?0;
????for?(;?i???????request.pendingChunks--;
??????var?_chunk2?=?errorChunks[i];
??????if?(!writeChunk(destination,?_chunk2))?{
????????request.flowing?=?false;
????????i++;
????????break;
??????}
????}
????errorChunks.splice(0,?i);
??}?finally?{
????reentrant?=?false;
????completeWriting(destination);
??}
??flushBuffered(destination);
??if?(request.pendingChunks?===?0)?{
????//?We're?done.
????close(destination);
??}
}
至此,服務端的大概處理邏輯就走完了。

接下來我們來看一下客戶端的處理邏輯:
客戶端邏輯
客戶端在獲取響應后,會開始反序列化,從根節(jié)點開始一個個進行處理返回的chunk,并渲染對應的組件:
function?createResponse()?{
??var?chunks?=?new?Map();
??var?response?=?{
????_chunks:?chunks,
????readRoot:?readRoot
??};
??return?response;
}
chunk在反序列化過程中,主要也是分為服務端組件和客戶端組件。對于服務器組件來說,由于之前返回的數(shù)據(jù)已經(jīng)是經(jīng)過渲染過的JSON元數(shù)據(jù),直接進行JSON.parse反序列化就可以了。
//?初始化服務端組件
function?initializeModelChunk(chunk)?{
??//?由于value是已經(jīng)渲染好的中間狀態(tài)數(shù)據(jù),直接反序列化
??//?如:?["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col?sidebar","children":?[...]}}]]
??var?value?=?parseModel(chunk._response,?chunk._value);
??var?initializedChunk?=?chunk;
??initializedChunk._status?=?INITIALIZED;
??initializedChunk._value?=?value;
??return?value;
}
而對于客戶端組件,由于返回的是webpack中相對地址信息,則直接調(diào)用webpack中的`webpack_require去獲?。?/p>
function?initializeModuleChunk(chunk)?{
??//?直接通過webpack獲取客戶端組件
??var?value?=?requireModule(chunk._value);
??var?initializedChunk?=?chunk;
??initializedChunk._status?=?INITIALIZED;
??initializedChunk._value?=?value;
??return?value;
}
接下來是一個大概的一個流程圖:

總結(jié)
優(yōu)勢
交互式富應用與能夠快速響應的靜態(tài)應用之間要如何選擇,有時候難以抉擇。但是有了RSC后,這兩者似乎可以達到一個很好的平衡。目前已經(jīng)的一些好處有:
打包體積大大減少,極好地提高頁面加載性能。
借助RSC,可以消除客戶端和服務器之間的邊界。比方說常見的CMS應用,你可以選擇在服務器上渲染文章,但是本地可以對其進行編輯。真正的讓React做到“一統(tǒng)天下“。
在網(wǎng)速較慢的環(huán)境下,服務器組件會以suspense的形式下發(fā)給客戶端,從而客戶端能夠得到過渡階段的響應。
如果說之前使用SSR會存在狀態(tài)丟失的問題,那么使用Server Components就可以完全避免這些問題了。
問題
RSC同SSR不同的是,它并不會將組件渲染成HTML的形式,因此對于SEO來說應該是無能為力的。所以它可能只適用于非首屏頁面。
引入RSC后,服務端和客戶端開始緊耦合,雖然利用命名規(guī)范可以區(qū)分服務器組件和客戶端組件,但是在編碼上容易造成額外的心智負擔。
同時,因為服務器組件是無狀態(tài)的,因此需要去思考新的編程范式和最佳實踐。從目前的官方的實現(xiàn)方式上看,由于最終會涉及到JSON序列化,因此RSC必須足夠簡單,才能在反序列化的過程中不丟失信息。這可能也會限制其能力。
最后由于服務器組件涉及到服務端,調(diào)試似乎也會變得困難。
未來可能的實踐與應用
我們先來看一下在現(xiàn)有的前端應用中一個簡單的無服務架構(gòu)是怎么做的。首先,我們會將靜態(tài)網(wǎng)頁部署在云端存儲中上,為了提高性能可能還會引入CDN加速等技術(shù), 而這些靜態(tài)資源在后續(xù)訪問的API資源會通過網(wǎng)關(API Gateway)的形式訪問無服務后端資源(Faas + Node.js),這些后端應用可以直接返回云數(shù)據(jù)庫的數(shù)據(jù)內(nèi)容。

那么在有了服務器組件后,我們可以在云端部署動態(tài)的服務器組件(可以作為Faas應用存在),以服務的形式提供給前端。

目前已經(jīng)有人對RSC的Serverless模式進行了實驗,感興趣的可以參考 https://github.com/sw-yx/amplify-react-serverless-components 這個項目。未來更多的可能發(fā)展方向:
React Components as a Service:組件即服務
Server React Components Marketplace:組件市場
Server React Components Theme Marketplace:組件主題市場
之后可以借助云端基礎設施,應用在可擴展、災備上也能夠得到進一步提升。
參考文檔
https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md
https://github.com/reactjs/server-components-demo
https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html
https://developers.google.com/web/updates/2019/02/rendering-on-the-web
轉(zhuǎn)載請保留這部分內(nèi)容,注明出處。
另外,頭條號前端團隊非常 期待你的加入
公眾號:頭號前端字節(jié)招聘 | 期待你的加入
