跳到主要內容
黯羽輕揚每天積累一點點

Render-as-You-Fetch

免費2019-12-01#JS#react并行加载数据#react渲染流程优化#react Render-as-You-Fetch#react数据加载最佳实践#渲染时并行加载数据

為什麼 Render-as-You-Fetch 更快?

一。Fetch-on-Render

一直以來,我們所遵從的最佳實踐都是 Fetch-on-Render 模式,即:

  1. 渲染組件(render)時發現沒有數據,就先顯示 loading

  2. componentDidMount 時發送請求(fetch)

  3. 數據回來之後開始渲染數據

這樣做的好處在於按關注點組織代碼,數據請求和數據對應的 UI 渲染邏輯放在一塊兒。但缺點也很明顯:

  • 串行:整個過程是串行的(先 render 後 fetch),導致越深層的數據越晚加載

  • fetch 與 render 綁定:意味著 lazy 組件的 fetch 時機也被 lazy 了,組件按需加載有了性能負擔

顯然,數據請求能夠先行,fetch 與 render 綁定也並不合理。但在開始優化之前,先考慮一個問題:我們想要實現的目標是什麼?

二。魚和熊掌的抉擇

就用戶體驗而言,我們想要達到的效果是:

  • 盡早顯示最重要的內容

  • 同時也不希望次要內容拖慢整頁(完整內容)加載時間

既要一部分內容優先展示,又不希望其餘內容因為優先級而延遲展示。似乎是個魚和熊掌的抉擇,但並行性讓二者兼得成為了可能,對應到技術實現上:

  • 數據和代碼都應該(按重要程度)增量加載

  • 而且最好並行

於是,Render-as-You-Fetch 模式出現了

三。Render-as-You-Fetch

具體的,Render-as-You-Fetch 模式分為 4 點:

  • 分離數據依賴:並行加載數據、創建視圖

  • 盡早加載數據:在事件處理函數中加載數據

  • 增量加載數據:優先加載重要數據

  • 盡早加載代碼:把代碼也看成數據

前三點針對數據加載的 what、when 與 how,最後一點針對 view。因為如果 data 已經足夠快了,view 也要跟上,畢竟 v = f(d)

P.S. 關於 v = f(d) 的更多信息,見 深入 React

分離數據依賴:並行加載數據、創建視圖

fetch 與 render 綁定,導致數據加載的 how 與 when 都受限於 render,是第一大阻礙因素。所以先要把數據依賴從 view 中抽離出來,把 what(要加載的數據)與 how(加載方式)和 when(加載時機)分開

The key is that regardless of the technology we're using to load our data — GraphQL, REST, etc — we can separate what data to load from how and when to actually load it.

有兩種實現方式,要麼人工分離,要麼靠構建工具來自動提取:

  • 定義同名文件:比如把 MyComponent.jsx 對應的數據請求放在 MyComponent.data.js

  • 編譯時提取數據依賴:數據請求還放在組件定義中,由編譯器來解析提取其中的數據依賴

後者在分離數據依賴的同時,還能兼顧組件定義的內聚性,是 Relay 所採用的做法:

// Post.js
function Post(props) {
  // Given a reference to some post - `props.post` - *what* data
  // do we need about that post?
  const postData = useFragment(graphql`
    fragment PostData on Post @refetchable(queryName: "PostQuery") {
      author
      title
      # ...  more fields ...
    }
  `, props.post);

  // Now that we have the data, how do we render it?
  return (
    <div>
      <h1>{postData.title}</h1>
      <h2>by {postData.author}</h2>
      {/* more fields  */}
    </div>
  );
}

由 Relay Compiler 把組件中的 GraphQL 數據依賴提取出來,甚至還能進一步聚合,把細碎的請求整合成一條 Query

盡早加載數據:在事件處理函數中加載數據

數據和視圖分開之後,二者可以並行獨立加載,那麼,什麼時機開始加載數據呢?

當然是盡可能早,所以要在接到交互事件(比如點擊、切換 tab、打開模態窗)後,同時分頭加載代碼和數據

The key is to start fetching code and data for a new view in the same event handler that triggers showing that view.

對於頁面級數據,可以交給路由統一控制數據加載時機,例如:

// Manually written logic for loading the data for the component
import PostData from './Post.data';

const PostRoute = {
  // a matching expression for which paths to handle
  path: '/post/:id',

  // what component to render for this route
  component: React.lazy(() => import('./Post')),

  // data to load for this route, as function of the route
  // parameters
  prepare: routeParams => {
    const postData = preloadRestEndpoint(
      PostData.endpointUrl,
      {
        postId: routeParams.id,
      },
    );
    return { postData };
  },
};

export default PostRoute;

甚至還可以在 hover、mousedown 之類的更早時機進行預加載

If we can load code and data for a view after the user clicks, we can also start that work before they click, getting a head start on preparing the view.

此時,可以考慮把預加載能力集中到 router 或核心 UI 組件中,因為預加載特性是否開啟通常取決於用戶的設備和網絡情況,集中管理更好控制

增量加載數據:優先加載重要數據

如果數據加載時機已經足夠早了,還有辦法加快速度嗎?

有。體驗上,我們傾向於優先展示更重要的 view,而不等所有數據都回來

But we still want to be able to show more important parts of the view without waiting for all of our data.

為此,Facebook 在 GraphQL 中實現了 @defer 指令:

// Post.js
function Post(props) {
  const postData = useFragment(graphql`
    fragment PostData on Post {
      author
      title

      # fetch data for the comments, but don't block on it being ready
      ...CommentList @defer
    }
  `, props.post);

  return (
    <div>
      <h1>{postData.title}</h1>
      <h2>by {postData.author}</h2>
      {/* @defer pairs naturally with <Suspense> to make the UI non-blocking too */}
      <Suspense fallback={<Spinner/>}>
        <CommentList post={postData} />
      </Suspense>
    </div>
  );
}

流式返回數據,優先提供非 @defer 字段,相當於數據層面的 Suspense 特性。這種思路同樣適用於 REST API,比如將數據字段按優先級分組,拆成兩個請求並行發送,避免不重要的數據拖慢重要數據

盡早加載代碼:把代碼也看成數據

做完所有的這一切,數據加載方面似乎已經達到極限了

然而,另一個不容忽視的因素是 React.lazy 只在實際渲染時才加載(組件)代碼,是代碼層面的 Fetch-on-Render:

React.lazy won't start downloading code until the lazy component is actually rendered.

類似的,可以把代碼也看成數據,交給路由來控制代碼加載時機,而不由 render 流程來決定

四。示例

五。總結

提升加載速度的關鍵在於盡早、增量地加載代碼和數據

Start loading code and data as early as possible, but without waiting for all of it to be ready.

具體分為 4 點:

  • 分離數據依賴:在加載 view(代碼)的同時,並行加載其所需數據

  • 盡早加載數據:接到交互事件後立即加載數據,甚至還能預判用戶行為,預加載 view

  • 增量加載數據:優先加載重要數據,但又不影響次要數據的加載速度

  • 盡早加載代碼:把(組件)代碼也看成數據,通過類似的方式來提升其加載速度

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論