<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          性能優(yōu)化竟白屏,難道真是我的鍋?

          共 16515字,需瀏覽 34分鐘

           ·

          2021-08-10 12:37

          項目日漸“強(qiáng)壯”,性能優(yōu)化方法之一是采用 React 框架提供的 Reat.lazy() 按需加載的方式,測試過程中,QA說我的優(yōu)化代碼導(dǎo)致了白屏,且看我如何狡辯~

          隨著項目日漸“強(qiáng)壯”,優(yōu)化首屏加載渲染速度迫在眉睫,其中就采用了 React 框架提供的 Reat.lazy() 按需加載的方式,測試過程中,在我們的埋點監(jiān)控平臺上,發(fā)現(xiàn)了很多網(wǎng)絡(luò)請求錯誤的日志,大部分來自分包資源下載失敗!難道我的優(yōu)化變成負(fù)優(yōu)化了???

          通過我們的統(tǒng)計平臺量化數(shù)據(jù)可知,用戶網(wǎng)絡(luò)加載失敗的概率還是比較大,實驗發(fā)現(xiàn),沒法兒使用 try{}catch{} 捕獲組件渲染錯誤,查詢官方文檔,有一個 Error Boundaries 的組件引入眼簾,提供了解決方法,那我們拿到了 demo 應(yīng)該怎么完善并應(yīng)用到我們的項目中,以及如何解決按需加載組件失敗的場景。

          一、背景

          某天我在開發(fā)了某個功能組件時,發(fā)現(xiàn)這個組件引用了一個非常大的三方庫,大概100kb,這么大,當(dāng)然得使用按需加載啦,當(dāng)我理所當(dāng)然地覺得這一手“按需加載”的優(yōu)化很穩(wěn),就交給測試同學(xué)測試了。

          沒過多久測試同學(xué)反饋,你這個功能咋老白屏?—— 怎么可能?我的代碼不可能有BUG!

          來到“事故現(xiàn)場”,稍加思索,打開瀏覽器控制臺,發(fā)現(xiàn)按需加載的遠(yuǎn)程文件下載失敗了。

          emmm~,繼續(xù)狡辯,這肯定是公司基建不行啊,網(wǎng)絡(luò)這么不穩(wěn),這鍋我不背!雖然極力狡辯,可是測試同學(xué)就不相信,就認(rèn)定了是我的問題...

          凡事講證據(jù),冷靜下來想一想,萬一真的是我的問題,豈不是很尷尬?

          為了挽回局面,于是強(qiáng)裝鎮(zhèn)定說到:“這個問題是網(wǎng)絡(luò)波動導(dǎo)致,雖然咱們的基建環(huán)境不太好,但是為了盡可能提升用戶體驗,我這嘗試下看看如何優(yōu)化,可通過增加錯誤監(jiān)控重試機(jī)制,增強(qiáng)用戶體驗,追求極致!”,趕緊溜回去看看咋解決吧...

          一、Error Boundaries

          React官方對于“Error Boundaries”的介紹:https://reactjs.org/docs/error-boundaries.html

          A JavaScript error in a part of the UI shouldn’t break the whole app. To solve this problem for React users, React 16 introduces a new concept of an “error boundary”.

          簡單翻譯,在 UI 渲染中發(fā)生的錯誤不應(yīng)該阻塞整個應(yīng)用的運行,為此,React 16 中提供了一種新的概念“錯誤邊界”。

          也就是說,我們可以用“錯誤邊界”來優(yōu)雅地處理 React 中的 UI 渲染錯誤問題。

          React 中的懶加載使用Suspense包裹,其下的子節(jié)點發(fā)生了渲染錯誤,也就是下載組件文件失敗,并不會拋出異常,也沒法兒捕獲錯誤,那么用 ErrorBoundary 就正好可以決定再子節(jié)點發(fā)生渲染錯誤(常見于白屏)時候的處理方式。

          注意:Error boundaries 不能捕獲如下類型的錯誤:

          • 事件處理(了解更多)
          • 異步代碼 (例如 setTimeout 或 requestAnimationFrame 回調(diào))
          • 服務(wù)端渲染
          • 來自ErrorBoundary組件本身的錯誤 (而不是來自它包裹子節(jié)點發(fā)生的錯誤)

          二、借鑒

          老夫作為“CV工程師”,自然是信手拈來:

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasErrorfalse };
            }

            static getDerivedStateFromError(error) {
              // Update state so the next render will show the fallback UI.
              return { hasErrortrue };
            }

            componentDidCatch(error, errorInfo) {
              // You can also log the error to an error reporting service
              logErrorToMyService(error, errorInfo);
            }

            render() {
              if (this.state.hasError) {
                // You can render any custom fallback UI
                return <h1>Something went wrong.</h1>;
              }

              return this.props.children; 
            }
          }

          使用方法:

          <ErrorBoundary>
            <MyWidget />
          </ErrorBoundary>
          • static getDerivedStateFromError(error):在 render phase 階段,子節(jié)點發(fā)生UI渲染拋出錯誤時候執(zhí)行,return 的 {hasError: true} 用于更新 state 中的值,不允許包含副作用的代碼,觸發(fā)重新渲染(渲染fallback UI)內(nèi)容。
          • componentDidCatch(error, errorInfo):在commit phase 階段,捕獲子節(jié)點中發(fā)生的錯誤,因此在該方法中可以執(zhí)行有副作用的代碼,例如用于打印上報錯誤日志。

          官方案例在線演示地址:https://codepen.io/gaearon/pen/wqvxGa?editors=0010

          與此同時官方的建議:

          In the event of an error, you can render a fallback UI with componentDidCatch() by calling setState, but this will be deprecated in a future release. Use static getDerivedStateFromError() to handle fallback rendering instead.

          推薦大家在 getDerivedStateFromError() 中處理 fallback UI,而不是在 componentDidCatch() 方法中,componentDidCatch() 在未來的 React 版本中可能會被廢棄,當(dāng)然只是推薦,僅供參考。

          三、修飾

          官方的 demo 組件如果要嵌入業(yè)務(wù)代碼中,還是有一些簡陋,為了更好地適應(yīng)業(yè)務(wù)代碼以及更加通用,我們一步步來改造。

          3.1 支持自定義fallback以及error callback

          目標(biāo):滿足些場景下,開發(fā)者需要自行設(shè)置 fallback 的UI,以及自定義錯誤處理回調(diào)

          實現(xiàn)也非常簡單,基于 TypeScript,再加上一些類型聲明,一個支持自定義fallback 和錯誤回調(diào)的 ErrorBoundary 就OK了!

          type IProps = {
            fallback?: ReactNode | null;
            onError?: () => void;
            children: ReactNode;
          };

          type IState = {
            isShowErrorComponent: boolean;
          };

          class LegoErrorBoundary extends React.Component<IProps, IState> {
            static getDerivedStateFromError(error: Error) {
              return { isShowErrorComponent: true };
            }

            constructor(props: IProps | Readonly<IProps>) {
              super(props);
              this.state = { isShowErrorComponent: false };
            }

            componentDidCatch(error: Error) {
              this.props.onError?.();
            }

            render() {
              const { fallback, children } = this.props;
              if (this.state.isShowErrorComponent) {
                if (fallback) {
                  return fallback;
                }
                return <>加載失敗,請刷新重試!</>;
              }
              return children;
            }
          }

          export default LegoErrorBoundary;

          3.2 支持錯誤手動重試

          我們的按需加載組件就像局部組件更新一樣,當(dāng)組件按需加載的渲染失敗時候,理論上我們應(yīng)該給用戶提供手動/自動重試機(jī)制

          手動重試機(jī)制,簡單的做法就是,在 fallback UI 中設(shè)置重試按鈕

            static getDerivedStateFromError(error: Error) {
              return { isShowErrorComponent: true };
            }

            constructor(props) {
              super(props);
              this.state = { isShowErrorComponent: false };
          +   this.handleRetryClick = this.handleRetryClick.bind(this);
            }
              
          + handleRetryClick() {
          +  this.setState({
          +    isShowErrorComponent: false,
          +  });
          + }

           render() {
            const { fallback, children } = this.props;
            if (this.state.isShowErrorComponent) {
              if (fallback) {
                return fallback;
              }
          +    return (
          +       <div>
          +        {/* CSS重置下按鈕樣式 */}
          +        <button className="error-retry-btn" onClick={this.handleRetryClick}>
          +          渲染錯誤,請點擊重試!
          +        </button>
          +      </div>
          +    );
             }
             return children;
           }

          寫一個普通的Counter(計數(shù)器)組件:

          import React, { useState } from 'react';

          const Counter = (props) => {
              const [count, setCount] = useState(0);

              const handleCounterClick = () => {
                  setCount(count+1);
              }

              const thr = () => {
                  throw new Error('render error')
              }

              return (
                  <div>
                      {count === 3 ?  thr() : ''}
                      計數(shù)器:{count}
                      <br/>
                      <button onClick={handleCounterClick}>點擊+1</button>
                  </div>

              )
          }

          export default Counter;

          我們使用這個 LegoErrorBoundary 組件包裹 Counter 計數(shù)器組件,Counter 組件中在第三次點擊時候拋出一個異常,來看看 ErrorBoundary 的捕獲處理情況!

          表現(xiàn)效果:

          如果咱不處理這個錯誤,就會導(dǎo)致“白屏”,也不利于研發(fā)同學(xué)排查問題,特別是涉及到一些異步渲染的問題。

          3.3 支持發(fā)生錯誤自動重試渲染有限次數(shù)

          手動重試,會增加用戶的一個操作,這會增加用戶的操作成本,為了更加便捷用戶使用軟件,提升用戶體驗,來瞅瞅采用自動重試有限次數(shù)的機(jī)制應(yīng)該如何實現(xiàn)。

          實現(xiàn)思路:

          重試次數(shù)統(tǒng)計變量:retryCount,記錄重試渲染次數(shù),超過次數(shù)則使用兜底渲染“錯誤提示”UI。

          改造如下:

          type IState = {
            isShowErrorComponent: boolean;
          + retryCount: number;
          };

          class LegoErrorBoundary extends React.Component<IProps, IState> {
          -  static getDerivedStateFromError(error: Error) {
          -    return { isShowErrorComponent: true };
          -  }
            
            constructor(props: IProps | Readonly<IProps>) {
              super(props);
          +   this.state = { isShowErrorComponent: false, retryCount: 0 };
          +  this.handleErrorRetryClick = this.handleErrorRetryClick.bind(this);
            }

            componentDidCatch(error: Error) {
          +  if (this.state.retryCount > 2) {
          +      this.setState({
          +        isShowErrorComponent: true,
          +      })
          +    } else {
          +      this.setState({
          +        retryCount: this.state.retryCount + 1,
          +      });
          +    }
            }

            render() {
              const { fallback, children } = this.props;
              if (this.state.isShowErrorComponent) {
                if (fallback) {
                  return fallback;
                }
          +      return <>重試3次后,展示兜底錯誤提示!</>;
              }
              return children;
            }
          }

          export default LegoErrorBoundary;

          來看看效果:

          自動重試3次

          改改Counter組件的代碼,看看能否處理好異步錯誤的問題:

          import React, { useEffect, useState } from 'react';

          const Counter = (props) => {
            const [count, setCount] = useState(0);

            const handleCounterClick = () => {
              setCount(count + 1);
            }

            const thr = () => {
              throw new Error('render error')
            }

            useEffect(() => {
              setTimeout(() => {
                setCount(3)
              }, 1000);
            }, []);

            return (
              <div>
                { count === 3 ? thr() : '' }
                計數(shù)器:{ count }
                <br />
                <button onClick={ handleCounterClick }>點擊+1</button>
              </div>
            )
          }

          export default Counter;

          表現(xiàn):

          處理異步發(fā)生的錯誤

          也是OK的!這說明,屬于業(yè)務(wù)邏輯的代碼比如:網(wǎng)絡(luò)數(shù)據(jù)請求、異步執(zhí)行導(dǎo)致渲染出錯的情況,“錯誤邊界”組件都是可以攔截并處理。

          當(dāng)前結(jié)論:使用 Errorboundary 組件包裹,能夠 handle 住子組件發(fā)生的渲染 error。

          四、異步加載組件網(wǎng)絡(luò)錯誤

          4.1 嘗試處理

          App.js 中的 Counter 組件引用改為按需加載,然后在瀏覽器中模擬分包的組件下載失敗情況,看看是否能夠攔住!

          const LazyCounter = React.lazy(() => import('./components/counter/index'));

          function App({
            return (
              <div className="App">
                <header className="App-header">
                  <img src={ logo } className="App-logo" alt="logo" />
                  <ErrorBoundary>
                    <LazyCounter></LazyCounter>
                  </ErrorBoundary>
                </header>
              </div>

            );
          }

          結(jié)果白屏了!也可以看到 ErrorBoundary 組件中打印了捕獲到的錯誤信息:

          ChunkLoadError: Loading chunk 3 failed.
          (error: http://localhost:5000/static/js/3.18a27ea8.chunk.js)
          at Function.a.e ((index):1)
          at App.js:7
          at T (react.production.min.js:18)
          at Hu (react-dom.production.min.js:269)
          at Pi (react-dom.production.min.js:250)
          at xi (react-dom.production.min.js:250)
          at _i (react-dom.production.min.js:250)
          at vi (react-dom.production.min.js:243)
          at fi (react-dom.production.min.js:237)
          at Gi (react-dom.production.min.js:285)

          攔截到了,但是沒有觸發(fā)3次重試,componentDidCatch 中的 console.log('發(fā)生錯誤!', error); 只打印了一次錯誤日志,就掛了,看到大家的推薦做法是,發(fā)生一次錯誤就能夠處理到,所以嘗試把 retryCount0 的時候就設(shè)置 isShowErrorComponent 的值,

          this.setState({
              isShowErrorComponenttrue,
          })

          這時能夠顯示錯誤的fallback UI:

          image

          但沒法兒實現(xiàn)自動重試有限次數(shù)異步組件的渲染,否則如果還按照之前的方案,就會繼續(xù)向上拋出錯誤,如果沒有后續(xù) catch 處理錯誤,頁面就會白屏!

          然后嘗試主動觸發(fā)重新渲染,發(fā)現(xiàn)并沒有發(fā)起二次請求,點擊重試只是捕獲到了錯誤~

          4.2 定位原因

          不生效,于是想到聲明引入組件的代碼如下:

          const LazyCounter = React.lazy(() => import('./components/counter/index'));

          經(jīng)過測試驗證,的確打印了錯誤日志,而只發(fā)起了一次網(wǎng)絡(luò)請求的原因是,該 LazyCounter 組件并沒有在組件中聲明,重新渲染的時候,LazyCounter 組件作為組件外的全局變量,不受 rerender 影響。

          4.3 解決方案

          因此,想要解決網(wǎng)絡(luò)加載錯誤問題并重試,就得在聲明代碼 import 的時候處理,因為import 返回的是一個Promise,自然就可以用 .catch 捕獲異常。

          - const LazyCounter = React.lazy(() => import('./components/counter/index'));

          + const LazyCounter = React.lazy(() => import('./components/counter/index').catch(err => {
          +   console.log('dyboy:', err);
          + }));

          import() 代碼執(zhí)行的時候才會觸發(fā)網(wǎng)絡(luò)請求拉取分包資源文件,所以我們可以在異常捕獲中重試,并且可以重試一定次數(shù),所以需要實現(xiàn)一個工具函數(shù),統(tǒng)一處理 import() 動態(tài)引入可能失敗的問題。

          該工具函數(shù)如下:

          /**
           * 
           * @param {() => Promise} fn 需要重試執(zhí)行的函數(shù)
           * @param {number} retriesLeft 剩余重試次數(shù)
           * @param {number} interval 間隔重試請求時間,單位ms
           * @returns Promise<any>
           */

          export const retryLoad = (fn, retriesLeft = 5, interval = 1000) => {
            return new Promise((resolve, reject) => {
              fn()
                .then(resolve)
                .catch(err => {
                  setTimeout(() => {
                    if (retriesLeft === 1) {
                      // 遠(yuǎn)程上報錯誤日志代碼
                      reject(err);
                      // coding...
                      console.log(err)
                      return;
                    }
                    retryLoad(fn, retriesLeft - 1, interval).then(resolve, reject);
                  }, interval);
                });
            });
          }

          使用的時候只需要將 import() 包一下:

          const LazyCounter = React.lazy(() => retryLoad(import('./components/counter/index')));

          與此同時,為了多次請求下,“錯誤邊界”組件能夠捕獲到錯誤,同時能夠觸發(fā)兜底渲染邏輯,把 ErrorBoundary 組件發(fā)生錯誤時候直接處理展示兜底邏輯,不做重復(fù)渲染。則將 ErrorBoundary 中的重渲染計數(shù)邏輯代碼刪除即可。

          componentDidCatch(error) {
            console.log('發(fā)生錯誤!', error);
            this.setState({
              isShowErrorComponenttrue,
            });
          }

          另外,如果我們既想要渲染出錯后的重試,還需要保證多次網(wǎng)絡(luò)出錯后能有錯誤上報,那么只需要在 retryLoad 工具函數(shù)中增加錯誤日志遠(yuǎn)程上報邏輯,然后不拋出 reject()。

          4.4 表現(xiàn)效果

          處理如下三種情況的效果:

          1. 正常按需加載組件成功
          2. 網(wǎng)絡(luò)原因一直下載失敗,展示兜底錯誤
          3. 網(wǎng)絡(luò)原因,中途恢復(fù),展示正常功能

            錄制的GIf比較大,微信上無法展示,可點擊閱讀全文查看效果!

            當(dāng)我把網(wǎng)絡(luò)加載失敗后的處理結(jié)果給到QA同學(xué),QA同學(xué)贊許地說道:“老哥,穩(wěn)!

          五、總結(jié)

          通過針對業(yè)務(wù)優(yōu)化場景中遇到的加載失敗問題,嘗試借助 ErrorBoundary 以及 import() 網(wǎng)絡(luò)重試加載機(jī)制,保證了程序的健壯性,降低前端“白屏率”,換個角度說,一定層次上提升了用戶的體驗和質(zhì)量,對于前端工程的收益是較為明顯!

          在本次的問題處理過程中,其實還有一些值得探究的地方:

          1. ErrorBoundary 捕獲錯誤的原理是啥?為什么不能處理本身錯誤?
          2. ErrorBoundary 除了接收 JSX,是否可以擴(kuò)展接收組件等,是否 fallback 可以和函數(shù)聯(lián)動?
          3. ErrorBoundary 是否可以作為前端白屏監(jiān)控?或更多應(yīng)用場景?思考&擴(kuò)展一下?

          Reference

          • static getDerivedStateFromError
          • componentDidCatch
          • Suspense for Data Fetching (Experimental)

          瀏覽 22
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  极品漂亮人妻找猛男3p | 欧美日逼视频网址 | 中文字幕成人在线观看 | 国产三级在线观看完整版 | 久久综合13p |