Có gì mới với Server-Side Rendering trong React 16?

React 16 đã có mặt!!

Có rất nhiều thứ để nói (nhất là việc viết lại hoàn toàn Fiber), nhưng với tôi, nhưng cải thiện trong server-side render là nổi bật nhất.

Hãy cùng tôi điểm qua những cái mới và khác biệt với SSR trong React 16, hi vọng là bạn cũng phấn khích như tôi.

Cách SSR hoạt động trong React 15

Đầu tiên, chúng ta hãy cũng nhìn lại cách thức hoạt đông của SSR trong React 15. Để dùng SSR, bạn sẽ phải chạy một Node-based web server như xpress, Hapi, hoặc Koa, và bạn call renderToString để render cho root component cho một string, mà sau đó bạn sẽ ghi cho một response:

// using Express
import { renderToString } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='content'>");  
  res.write(renderToString(<MyPage/>));
  res.write("</div></body></html>");
  res.end();
});

Sau đó, trong client bootstrap code, bạn sẽ yêu cầu client-side renderer “rehydrate” lại server-generated HTML nhờ vào  render(), giống như phương thức với client-side rendered app:

import { render } from "react-dom"
import MyPage from "./MyPage"
render(<MyPage/>, document.getElementById("content"));

Nếu bạn làm đúng theo trên thì client-side renderer sẽ chỉ cần dùng các server-generated HTML có sẵn mà không cần phải update DOM.

Vậy còn SSR trong React 16 thì sao?

React 16 có tính tương thích ngược (Backwards Compatible)

Nói cách khác nếu code của bạn chạy ngon lành trên React 15 thì nó cũng sẽ “trơn tuột” trên cả React 16. Đoạn code ở mục trên cũng chạy được trên cả 2 phiên bản 15 và 16.

render() trở thành hydrate()

Khi bạn upgrade SSR code từ React 15 lên React 16, bạn có thể sẽ bắt gặp bảng warning như sau trong browser của mình:

Hóa ra trong React 16, giờ có tới 2 phương pháp khác nhau để render client side:  render() dành cho các nội dung nằm hoàn toàn về phía client, và  hydrate() cho khi bạn muốn reder server-side. Bởi vì React là backwards compatible,render() vẫn sẽ hoạt động bình thường khi render trên server-generated markup trong React 16, nhưng bạn nên thay những calls đến hydrate() để không bị warning cũng như chuẩn bị code cho phiên bản React 17 trong tương lai. Đoạn trích code bên dưới cũng sẽ thay đổi theo:

import { hydrate } from "react-dom"
import MyPage from "./MyPage"
hydrate(<MyPage/>, document.getElementById("content"));

React 16 có thể xử lí Arrays, Strings, và Numbers

Trong React 15, một component của render sẽ luôn phải trả về (return) một React element đơn. Tuy vậy, với React 16, client-side renderer cho phép components trả về một string, một number, hay một array từ render. Tính năng cũng được hỗ trợ bởi React 16’s server-side render.

Như vậy, giờ bạn đã có thể server-render components như sau:

class MyArrayComponent extends React.Component {
  render() {
    return [
      <div key="1">first element</div>, 
      <div key="2">second element</div>
    ];
  }
}
class MyStringComponent extends React.Component {
  render() {
    return "hey there";
  }
}
class MyNumberComponent extends React.Component {
  render() {
    return 2;
  }
}

Thậm chí còn có cả khả năng pass trong một string, number hoặc một array component tới level cao của  renderToString:

res.write(renderToString([
      <div key="1">first element</div>, 
      <div key="2">second element</div>
    ]));
// it’s not entirely clear why you would do this, but it works!
res.write(renderToString("hey there"));
res.write(renderToString(2));

Điều này cho phép bạn loại bỏ bất cứ  divs and span vừa được thêm vào trong cây React component của bạn, nhờ đó mà kích thước của HTML document sẽ được thu nhỏ lại.

React 16 cho ra những HTML hiệu quả cao

Khi nhắc tới kích thước của HTML document, React 16 cũng cắt gọt bớt SSR trong HTML. Với React 15, từng yếu tố của HTML trong một SSR document có  data-reactid attribute, luôn có giá trị tăng dần theo ID, và text node đôi khi bị bao quanh bởi các comment với  react-text và một ID. Đoạn trích code dưới đây chính là một ví dụ điển hình cho trường hợp trên:

renderToString(
  <div>
    This is some <span>server-generated</span> <span>HTML.</span>
  </div>
);

Trong React 15, đoạn code đấy sẽ cho ra HTML như sau:

<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>

Còn với React 16, Mọi IDs đã được loại bỏ khỏi markup, nên HTML sẽ trở nên vô cùng đơn giản:

<div data-reactroot="">
  This is some <span>server-generated</span> <span>HTML.</span>
</div>

Không chỉ nó dễ nhìn dễ đọc hơn mà còn giúp giảm kích thước của HTML document  một cách đáng kể!

React 16 cho phép Non-Standard DOM Attributes

DOM renderer trong React 15 khá là khắt khe trong attribute trên các yếu tố thuộc HTML, và nó sẽ tự động loại bỏ ra các non-standard HTML attributes. React 16 thì ngược lại, cả client và server renderer giờ sẽ cho qua cả các non-standard attributes mà bạn đã thêm vào HTML. Bạn có thể đọc thêm Dan Abramov’s React blog để hiểu rõ hơn.

React 16 SSR không hỗ trợ Error Boundaries hoặc Portals

Có hai tính năng mới trong React 16 client-side renderer nhưng lại không được hỗ trợ trong server-side renderer:Error Boundaries và Portals. Nếu bạn muốn biết thêm về error boundaries, hãy đọc Abramov’s React blog, nhưng hãy lưu ý rằng error boundaries sẽ không có bắt được lỗi trên server. Còn Portals thì tới giờ vẫn chưa có bài viết nào hay giải thích về nó nhưng tóm gọn thì Portal API cần một DOM node, nên nó cũng sẽ không dùng được server.

React 16 thực hiện Client-Side Checking nhẹ nhàng hơn

Khi bạn rehydrate markup trên client-side trong React 15, ReactDOM.render() sẽ thực hiện một so sách từng kí tự một với server-generated markup. Nếu có bất kì sai sót gì thì React sẽ phát warning trong development mode và thay toàn bộ cây server-generated markup với HTML được tạo ra bên phía client.

Trong React 16, client-side renderer sử dụng một thuật toán khác để kiểm tra xem server-generated markup có đúng hay không. Nó thật sự khoan hồng hơn so với React 15. Ví dụ như nó không cần server-generated markup phải có các attributes theo đúng thứ tự như phía client. Và nếu có sai sót thì thay vì thay toàn bộ thì nó chỉ nhắm vào sub tree ví trị sai đó thôi.

Nói chung, thay đổi này sẽ không ảnh hưởng đến end-user ngoài trừ một điều: React 16 không fix SSR-generated HTML attributes bị sai/lệch khi bạn call  ReactDOM.render()/hydrate(). Do đó mà bạn sẽ phải cẩn thận và chắc chắn rằng đã sửa hết mọi markup mismatch warning bạn thấy trong app trong  development mode.

React 16 không cần được Compiled thì mới có hiệu suất tốt nhất

Trong React 15, nếu bạn dùng SSR thẳng luôn thì hiệu suất của nó sẽ không được tốt như kì vọng, thậm chí là kể cả trong production mode. Đó là bởi vì có rất nhiều developer warnings và gợi ý trong React, chúng sẽ giống như thế này:

if (process.env.NODE_ENV !== "production") {
  // check some stuff and output great developer
  // warnings here.
}

Thật không may, process.env  lại không phải là một JavaScript object bình thường, và nó khá là tốn công để có thể lấy được một value từ nó. Do đó nên dù ta có set giá trị của NODE_ENV thành  production, chỉ việc checking môi trường của variable thường xuyên cũng đã thêm rất nhiều thời gian cho server rendering.

Để giải quyết vấn đề này trong React 15, bạn sẽ compile SSR code để lại bỏ references của  process.env, nhờ vào Environment Plugin của Webpack hoặc transform-inline-environment-variables plugin của Babel. Tuy vậy, theo kinh nghiệm của tôi, rất nhiều người không hề compile server-side code, đó đó hiệu năng SSR cho ra rất là tệ.

Trong React 16, vấn đề này đã được giải quyết. Chỉ cần một call để check process.env.NODE_ENV ngay từ khi mới bắt đầu, do đó mà không cần phải compile SSR code của bạn để có hiệu năng tốt nhất bởi bạn đã có nó ngay từ đầu rùi.

React 16 cũng chạy nhanh hơn

Nhắc đến tốc độ, các người dùng React server-side render trong production thường than phiền rằng document có kích thước lớn render khá chậm.

Nên tôi rất vui khi thông báo rằng, sau một vài bài test đã cho thấy tốc độ khá cao trong server-side render của React 16, trải dài qua các phiên bản Node hác nhau:

Khi so sánh với React 15 với  process.env được compiled, khác biệt là khoảng 2.4 lần trong Node 4, khoảng 3 lần trong Node 6 và gấp 3.8 trong Node 8.4. Còn nếu so sánh khi không compile thì React 15 vẫn bị React 16 cho hửi khói trong mọi phiên bản Node khác nhau.

Vì sao React 16 SSR lại nhanh tới vậy? Bởi vì với React 15, server và client rendering có code tương tự nhau. Điều đó có nghĩa toàn bộ data structures cần phải duy trì một virtual DOM khi server rendering, cho dù rằng vDOM sẽ bị bỏ đi ngay khi giá trị được return sau khi đã call đến renderToString. Kết quả là có quá nhiều thứ bị phung phí trong server render.

Trong React 16 thì mọi chuyện hoàn toàn khác, nhờ vào core team đã viết lại hoàn toàn server renderer, và không còn dính dáng tới vDOM nữa. Nhờ đó mà nó sẽ trở nên nhanh hơn rất nhiều.

Một điều cần lưu ý rằng, mặc dù kết quả so sánh phía trên đã chứng tỏ rằng React 16 hoàn toàn là kẻ thống trị mọi mặc nhưng do những bài test đều nhắm tới từng tính năng một. Mặt khác, một app khi được sử dụng sẽ bao gồm những tính năng chạy cùng một lúc vì thế nên các bạn đừng trông mong rằng nó sẽ chạy nhanh tới gấp 3 lần như theo kết quả của bài test.

React 16 hỗ trợ streaming

React 16 giờ đã hỗ trợ render trực tiếp cho một Node stream.

Việc Render một stream có thể giúp giảm thiểu thời gian xử lí first byte (TTFB) cho nội dung của bạn, đồng thời gửi một phần của document vào cho browser ngay cả trước khi phần tiếp theo được tạo ra. Các browser sẽ parsing và rendering nội dung sớm hơn khi stream từ server theo cách này.

Ngoài ra còn có một tính năng tuyệt vời khác là backpressure. Nói cách khác, khi network bị backed up và không thể nhận thêm bytes, renderer sẽ nhận được tín hiệu và dừng rendering cho đến khi hết bị tắc nghẽn. Điều đó có nghĩa là server của bạn dùng ít memory và đáp ứng nhanh hơn tới I/O conditions.

Để sử dụng React 16’s render cho stream, bạn sẽ cần call 1 trong 2 phương pháp mới trên react-dom/server: renderToNodeStream or renderToStaticNodeStream, nhằm tương xứng với  renderToString và renderToStaticMarkup. Thay vì trả về một string, chúng sẽ return Readable, là một Node Stream class cho objects phát ra một stream của byte.

Khi bạn nhận lại  Readable stream từ renderTo(Static)NodeStream, nó sẽ ở trong paused mode, và chưa hề được render. Render sẽ chỉ bắt đầu khi bạn call  read, hoặc pipe  Readable đến một  Writable stream. Phần lớn các Node web frameworks có một object kế thừa từ  Writable, để bạn có thể pipe Readable dễ dàng hơn.

// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='content'>"); 
  const stream = renderToNodeStream(<MyPage/>);
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write("</div></body></html>");
    res.end();
  });
});

Streaming giờ đã có một vài Gotchas

Trong khi rendering một stream kể thể nói là một bước tiến cải thiện đáng kể, vẫn có một vài SSR pattern không tương thích tốt với streaming.

Thông thường thì bất cứ pattern nào sử dụng server render pass để tạo ra markup (cần phải add vào document trước khi SSR-ed thực hiện) thì đều không tương thích với streaming. Một số ví dụ bao gồm frameworks có chức năng xác định CSS nào nên cho vào page trong một  <style> tag, hoặc frameworks add các yếu tố vào document <head>  trong khi render. Nếu bạn dùng những framework này thì bạn sẽ phải dùng string rendering để stream.

Một pattern khác không tương thích tốt với React 16 là embedding calls đến renderToNodeStream vào cây component. Trong React 15, chúng ta thường dùng renderToStaticMarkup để tạo ra page template và embed calls đến renderToString để tạo ra những nội dung mạnh mẽ:

res.write("<!DOCTYPE html>");
res.write(renderToStaticMarkup(
  <html>
    <head>
      <title>My Page</title>
    </head>
    <body>
      <div id="content">
        { renderToString(<MyPage/>) }
      </div>
    </body>
  </html>);

Tuy nhiên nếu bạn thay những render call này thì streaming sẽ không thể chạy được bởi nó vẫn còn chưa thể cho một  Readable stream (được return từ  renderToNodeStream) được embedded như là một yếu tố thuộc component.

Nguồn: blog.topdev.vn via Hackernoon