性能优化竟然是黑屏,真的是我的错吗?
时间:2023-03-16 02:18:28
科技观察
本文转载自微信公众号“DYBOY”,作者DYBOY。转载本文请联系DYBOY公众号。随着项目越来越强大,优化首屏加载和渲染速度迫在眉睫。其中,使用了React框架提供的Reat.lazy()按需加载方式。大量的网络请求错误日志,大部分来自分包资源下载失败!难道我的优化变成了负优化?根据我们统计平台的量化数据来看,用户网络加载失败的概率还是比较高的。实验发现没有办法使用try{}catch{}捕捉组件渲染错误,查看官方文档,有个ErrorBoundaries组件映入眼帘,提供了解决方案。然后我们得到如何改进demo并应用到我们的项目中,以及如何解决按需组件加载失败的场景。背景某天在开发一个功能组件的时候,发现这个组件引用了一个非常大的三方库,大约100kb,这么大,当然得用按需加载了。当我理所当然地认为这个手“按需加载”优化非常稳定,所以我将它交给测试学生进行测试。没过多久就测试了同学们的反馈。为什么你的函数总是空白?——怎么可能?我的代码不能有BUG!来到“事故现场”,稍作思索,打开浏览器控制台,发现按需加载远程文件下载失败。emmm~,继续狡辩,这肯定是公司基础设施不好,网络太不稳定了,怪不得我!虽然我极力狡辩,但考的同学不信,就认定是我的问题……一切都是有据可依的,静下心来想一想,如果真的是我的问题,会不会呢?不尴尬吗?为了挽回局面,我故作镇定地说:“这个问题是网络波动引起的,虽然我们的基础设施环境不是很好,但是为了尽可能的提高用户体验,还是让我试试看如何优化吧,加入错误监听重试机制,提升用户体验,追求完美!”,赶快回去看看如何解决吧...1.ErrorBoundariesReact官方关于《关于》的介绍错误边界”:https://reactjs.org/docs/error-boundaries.htmlUI一部分中的JavaScript错误不应破坏整个应用程序。为了为React用户解决这个问题,React16引入了“错误边界”的新概念。简单翻译一下,UI渲染过程中出现的错误不应该阻塞整个应用程序的运行。为此,React16提供了“错误边界”的新概念。也就是说,我们可以使用“错误边界”来优雅地处理React中的UI渲染错误。React中的懒加载使用的是Suspense包,其下的子节点出现渲染错误,即组件文件下载失败,并没有抛出异常,也没有办法捕获错误,所以ErrorBoundary可以正好用来判断子节点出现渲染错误(常见于白屏)时的处理方式。注意:错误边界无法捕获以下类型的错误:事件处理(了解更多)异步代码(例如setTimeout或requestAnimationFrame回调)来自ErrorBoundary组件本身的服务器端呈现错误(不是来自其包装的子节点中发生的错误)2.作为“CV工程师”自然要向老大学习:classErrorBoundaryextendsReact.Component{constructor(props){super(props);this.state={hasError:false};}staticgetDerivedStateFromError(error){//UpdatetestatesothenextrenderwillshowthefallbackUI.return{hasError:true};}componentDidCatch(error,errorInfo){//你也可以将错误记录到错误报告服务logErrorToMyService(error,errorInfo);}render(){if(this.state.hasError){//你可以渲染任何自定义的fallbackUIreturn
Somethingwentwrong.
;}返回这个。props.children;}}使用方法:
staticgetDerivedStateFromError(error):在renderphase阶段,当子节点UI渲染抛出错误时,执行,{hasError:true}的return用于更新状态中的值,不允许包含副作用的代码触发重新渲染(渲染回退UI)内容。componentDidCatch(error,errorInfo):在commitphase阶段,捕获子节点发生的错误,所以有副作用的代码可以在这个方法中执行,比如打印和上报错误日志。官方案例在线演示地址:https://codepen.io/gaearon/pen/wqvxGa?editors=0010同时,官方建议:如果出现错误,可以通过componentDidCatch()渲染一个fallbackUI调用setState,但这将在未来的版本中弃用。使用静态getDerivedStateFromError()来处理回退渲染。建议您在getDerivedStateFromError()而不是componentDidCatch()方法中处理回退UI。可能会过时,当然只是推荐,仅供参考。3、修改官方的demo组件如果想嵌入到业务代码中,还是有点简陋。为了更好的适配业务代码,使其更加通用,我们会逐步修改。3.1支持自定义回退和错误回调目标:在某些场景下,开发者需要自己设置回退UI,自定义错误处理回调的实现也很简单,基于TypeScript,加上一些类型声明,一个支持自定义回退和错误回调的ErrorBoundary是OK!typeIProps={fallback?:ReactNode|null;onError?:()=>void;children:ReactNode;};typeIState={isShowErrorComponent:boolean;};classLegoErrorBoundaryextendsReact.Component
{staticgetDerivedStateFromError(error:Error){return{isShowErrorComponent:true};}构造函数(props:IProps|Readonly){super(props);this.state={isShowErrorComponent:false};}componentDidCatch(error:Error){this.props.onError?.();}render(){const{fallback,children}=this.props;if(this.state.isShowErrorComponent){if(fallback){returnfallback;}return<>失败加载,请刷新后重试!>;}returnchildren;}}exportdefaultLegoErrorBoundary;3.2支持错误手动重试我们的按需加载组件就像部分组件更新一样。当组件按需加载渲染失败时,理论上我们应该为用户提供手动/自动重试机制Manualretrymechanism,简单的方法是在fallbackUI中设置重试按钮staticgetDerivedStateFromError(error:Error){return{isShowErrorComponent:真};}构造uctor(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){returnfallback;}+return(++{/*CSS重置按钮样式*/}++渲染错误,请点击重试!++
+);}returnchildren;}写一个普通的Counter(计数器)组件:importReact,{useState}from'react';constCounter=(props)=>{const[count,setCount]=useState(0);constandleCounterClick=()=>{setCount(count+1);}constthr=()=>{thrownewError('rendererror')}return({count===3?thr():''}counter:{count}
点击+1
)}exportdefaultCounter;我们用这个LegoErrorBoundary组件来包裹Counter计数器组件,第三次点击Counter组件会抛出异常,我们来看看ErrorBoundary的捕获处理情况!性能影响:如果我们不处理它这个错误会导致“白屏”,不利于研发同学排查问题,尤其是涉及到一些异步渲染的问题,会增加用户的操作成本。为了方便用户使用软件,提高用户体验,我们来看看如何实现使用有限次数自动重试的机制。实现思路:重试次数的统计变量:retryCount,记录渲染重试的次数,超过次数则在底部渲染“错误提示”UI。转换如下:typeIState={isShowErrorComponent:boolean;+retryCount:number;};classLegoErrorBoundaryextendsReact.Component{-staticgetDerivedStateFromError(error:Error){-return{isShowErrorComponent:true};-}constructor(props:IProps|Readonly){super(props);+this.state={isShowErrorComponent:false,retryCount:0};+this.handleErrorRetryClick=this.handleErrorRetryClick.bind(this);}componentDidCatch(错误:错误){+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){returnfallback;}+return<>重试3次后自底向上报错将显示消息!>;}returnchildren;}}exportdefaultLegoErrorBoundary;看效果:自动重试3次,改一下Counter组件的代码看能不能处理异步错误:importReact,{useEffect,useState}from'react';constCounter=(props)=>{const[count,setCount]=useState(0);constandleCounterClick=()=>{setCount(count+1);}constthr=()=>{thrownewError('rendererror')}useEffect(()=>{setTimeout(()=>{setCount(3)},1000);},[]);返回({count===3?thr():''}计数器:{count}
Click+1
)}exportdefaultCounter;performance:处理异步发生的错误也是可以的!这说明属于业务逻辑的代码,比如:网络数据请求,异步执行导致渲染错误,“错误边界”组件可以拦截处理当前结论:使用Errorboundary组件包,能够处理渲染错误发生在子组件中。4、异步加载组件网络错误4.1尝试处理将App.js中的Counter组件引用改为按需加载,然后在浏览器中模拟分包组件下载失败,看能否屏蔽!constLazyCounter=React.lazy(()=>import('./components/counter/index'));functionApp(){return( );}结果白屏!还可以看到ErrorBoundary组件打印Capturederrormessage:ChunkLoadError:Loadingchunk3failed.(error:http://localhost:5000/static/js/3.18a27ea8.chunk.js)atFunction.a.e((index):1)atApp.js:7atT(react.production.min.js:18)atHu(react-dom.production.min.js:269)atPi(react-dom.production.min.js:250)atxi(react-dom.production.min.js:250)at_i(react-dom.production.min.js:250)atvi(react-dom.production.min.js:243)atfi(react-dom.production.min.js:237)atGi(react-dom.production.min.js:285)拦截,但没有触发3次重试,console.log('发生错误!',eincomponentDidCatch错误);只打印一次错误日志后,就挂了。看到大家推荐的方法是一旦出错就可以处理,所以尝试在retryCount为0的时候设置isShowErrorComponent的值,this.setState({isShowErrorComponent:true,})这时候报错fallbackUI可以显示:但是没有办法自动重试异步组件的渲染次数有限。否则,如果仍然按照之前的计划,错误会继续往上抛。如果没有后续的catch处理Error,页面就会空白!然后尝试触发重新渲染,发现没有发起第二次请求。点击重试才抓到错误~4.2定位不生效的原因,于是想到了声明引入组件的代码如下:constLazyCounter=React.lazy(()=>import('./components/counter/指数'));经过测试验证,确实打印了错误日志,但是之所以只发起一次网络请求,是因为LazyCounter组件没有在组件中声明,重新渲染时,LazyCounter组件是组件外的全局变量,并且不会受到rerender4.3解决方案的影响所以,如果要解决网络加载错误的问题再重试,就得在声明代码import的时候处理一下,因为import的return是一个Promise,自然可以catchexceptionswith。抓住。-constLazyCounter=React.lazy(()=>import('./components/counter/index'));+constLazyCounter=React.lazy(()=>import('./components/counter/index').catch(err=>{+console.log('dyboy:',err);+}));当执行import()代码时,会触发网络请求拉取分包资源文件,所以我们可以捕获异常Retry,并且可以重试一定次数,所以需要实现一个实用函数来统一处理import()动态导入可能失败的问题。工具函数如下:/****@param{()=>Promise}fn需要重试的函数*@param{number}retriesLeft剩余重试次数*@param{number}interval间隔重试请求时间,单位ms*@returnsPromise
*/exportconstretryLoad=(fn,retriesLeft=5,interval=1000)=>{returnnewPromise((resolve,reject)=>{fn().then(resolve).catch(err=>{setTimeout(()=>{if(retriesLeft===1){//远程上报错误日志代码reject(err);//coding...console.log(err)return;}retryLoad(fn,retriesLeft-1,interval).then(resolve,reject);},interval);});});}使用的时候只需要包裹import():constLazyCounter=React.lazy(()=>retryLoad(import('./components/counter/index')));同时,对于多个请求,“ErrorBoundary”组件可以捕获错误,同时触发底层渲染逻辑,发生错误时直接处理显示ErrorBoundary组件的底层逻辑,不重复渲染.然后删除ErrorBoundary中的重新渲染计数逻辑代码。componentDidCatch(error){console.log('Anerroroccurred!',error);this.setState({isShowErrorComponent:true,});}另外,如果我们想在渲染出错后重试,需要保证多个times如果发生网络错误后有报错,只需要在retryLoad工具函数中加入错误日志远程上报逻辑,然后不要抛出reject()。4.4性能效果处理以下三种情况的效果:正常按需组件加载成功;网络原因不断下载失败,显示自下而上的错误;网络原因,中途恢复,显示正常功能