Sidecar用于代码拆分


代码拆分。 代码拆分无处不在。 但是,为什么呢? 只是因为现在有太多的javascript ,并且并非所有的JavaScript都在同一时间使用。


JS是一件很沉重的事情。 不适用于您的iPhone Xs或全新的i9笔记本电脑,但适用于数百万(可能是数十亿) 速度较慢的设备所有者。 或者,至少对于您的手表。


所以-JS不好,但是如果我们禁用它 ,会发生什么 -问题将消失……对于某些站点,对于基于React的站点,它会“随站点”消失。 但是无论如何-有些站点可以在没有JS的情况下运行...我们应该向他们学习一些东西...


代码拆分


今天,我们有两种方法,可以使它变得更好或不使它变得更糟:


1.写更少的代码


那是你最好的事情。 尽管React Hooks可以让您减少一些代码,而Svelte解决方案却可以使您生成的代码比平时少,但这并不是一件容易的事。


它不仅与代码有关,而且与功能有关-要使代码“紧凑”,就必须使其“紧凑”。 如果应用程序包做很多事情(并以20种语言提供),则无法保持它很小。


有写简短代码的方法,有写相反的实现的方法- 血腥的企业 。 而且,您都知道,两者都是合法的。



但是主要的问题-代码本身。 一个简单的react应用程序可以轻松绕过“推荐”的250kb。 您可能需要花费一个月的时间对其进行优化,然后将其缩小。 “小型”优化已得到充分证明,并且非常有用-只需获得具有size-limit bundle-analyzer并恢复其形状即可。
有许多库在为每个字节而战,试图将您限制在自己的范围内-preactstoreon ,仅举几例。


但是我们的应用程序超出了200kb。 接近100Mb 。 删除千字节毫无意义。 即使删除兆字节也没有任何意义。


片刻之后,将您的应用程序缩小是不可能的。 随着时间的推移,它将变得更大。

2.减少代码


或者, code split 。 换句话说- 投降 。 拿起100mb的捆绑包,然后用它制作20个5mb的捆绑包。 老实说-这是处理应用程序的唯一可能的方法-如果应用程序很大,请从中创建一堆较小的应用程序。


但是,您现在应该知道一件事:无论您选择什么选项,它都是实现细节,而我们正在寻找更可靠的东西。


关于代码拆分的真相


关于代码拆分的真相是,它的本质是TIME SEPARATION 。 您不仅在拆分代码,还以一种在单个时间点使用尽可能少的方式拆分代码。


只是不要立即发布您不需要的代码。 摆脱它。



说起来容易,很难做。 我有一些繁重但没有充分拆分的应用程序,其中任何页面的加载量占所有内容的50%。 有时, code splitting会变成code separation ,我的意思是-您可以将代码移到不同的块中,但仍然可以全部使用。 Recal说: “现在就不要发布您不需要的代码” ,-我需要 50%的代码,这才是真正的问题。


有时仅在此处添加import是不够的。 直到不是时间分隔,而只有空间分隔-完全没有关系。

有3种常见的代码拆分方式:


  1. 只是动态import 。 这些天很少使用。 更多有关跟踪状态的问题。
  2. Lazy组件,当您可以推迟渲染和加载React组件时。 这些天,大约90%的“反应代码拆分”。
  3. 懒惰 Library ,实际上是.1 ,但是您将通过React渲染道具获得库代码。 在react-imported-componentloadable-components中实现 。 很有用,但并不广为人知。

组件级代码拆分


这是最受欢迎的。 作为按路由的代码拆分或按组件的代码拆分。 做到这一点并保持良好的感知结果并非易事。 死于Flash of Loading Content


好的技术是:


  • 并行加载js chunk和路由data
  • 使用skeleton在页面加载之前显示类似于页面的内容(例如Facebook)。
  • prefetch块,甚至可以使用guess-js进行更好的预测。
  • 使用一些延迟,加载指示符, animationsSuspense (以供将来使用)来缓和过渡。

而且,这一切都与感知性能有关。



来自具有Ghost Elements的改进UX的图像

听起来不好


您知道,我可以称自己为代码拆分专家-但是我有自己的失败经历。


有时我可能无法减小捆绑包的大小。 有时,只要the _more_ code splitting you are introducing - the more you spatially split your page - the more time you need to _reassemble_ your page back ,我就可能无法提高结果的性能the _more_ code splitting you are introducing - the more you spatially split your page - the more time you need to _reassemble_ your page back *。 这就是所谓的波涛


  • 无需SSR或预渲染。 目前,正确的SSR可以改变游戏规则。


上周我遇到了两个失败:


  • 只要我的图书馆更好,但我却在一个图书馆比较中迷失了,但比另一个图书馆大得多。 我未能执行“ 1.编写更少的代码”
  • 优化一个由我妻子在React中制作的小型网站。 它使用的是基于路由的组件拆分,但是headerfooter保留在主包中,以使过渡更加“可以接受”。 仅几件事,彼此紧紧耦合在一起,使捆绑包的一面飙升到320kb(在gzip之前)。 没有什么重要的事情,也没有什么我能真正去除的。 死亡减少一千 。 我未能交付更少的代码

React-Dom是20%,core-js是10%,react-router,jsLingui,react-powerplug ...自己代码的20%...我们已经完成了。


解决方案


我已经开始考虑如何解决我的问题,以及为什么常见的解决方案无法在我的用例中正常工作。


我做了什么 我列出了所有关键位置,没有这些应用程序将根本无法工作,并试图理解为什么我拥有其余的位置。

这真是一个惊喜。 但是我的问题是在CSS中。 在原始CSS过渡中。


这是代码


  • 控制变量componentControl最终将设置为DisplayData应该显示的内容。
  • 一旦设置了值DisplayData变为可见,更改className ,从而触发花式过渡。 同时FocusLock激活,使DisplayData成为模态
     <FocusLock enabled={componentControl.value} // ^ it's "disabled". When it's disabled - it's dead. > {componentControl.value && <PageTitle title={componentControl.value.title}/>} // ^ it's does not exists. Also dead <DisplayData data={componentControl.value} visible={componentControl.value !== null} // ^ would change a className basing on visible state /> // ^ that is just not visible, but NOT dead </FocusLock> 

我想对这一部分进行整体编码,但是由于两个原因,我无法做到这一点:


  1. 一旦需要,信息应该立即可见,没有任何延迟。 业务需求。
  2. 信息“ chrome”应该存在之前,才能进行属性句柄转换。

使用CSSTransitionGrouprecondition可以部分解决此问题。 但是,您知道,固定一个代码再添加另一个代码听起来很奇怪,即使实际上足够了 。 我的意思是添加更多的代码可以帮助删除更多的代码。 但是...但是...


应该有更好的方法!

TL; DR-这里有两个关键点:


  • DisplayData必须被挂载 ,并且先于DOM存在。
  • FocusLock也应该事先存在,而不是导致DisplayData重新安装,但是一开始就不需要动脑筋



因此,让我们改变思维模式


蝙蝠侠和罗宾


假设我们的代码是蝙蝠侠和罗宾。 蝙蝠侠可以应付大多数坏人,但是当他无法应付时,他的搭档罗宾(Robin)便应命了。


蝙蝠侠再次参加战斗,罗宾将在稍后到达。

这是蝙蝠侠:


 +<FocusLock - enabled={componentControl.value} +> - {componentControl.value && <PageTitle title={componentControl.value.title}/>} + <DisplayData + data={componentControl.value} + visible={componentControl.value !== null} + /> +</FocusLock> 

这是他的搭档罗宾(Robin)::


 -<FocusLock + enabled={componentControl.value} -> + {componentControl.value && <PageTitle title={componentControl.value.title}/>} - <DisplayData - data={componentControl.value} - visible={componentControl.value !== null} - /> -</FocusLock> 

蝙蝠侠和罗宾可以组队 ,但实际上他们是两个不同的人。


并且不要忘记-我们仍在谈论代码拆分 。 而且,就代码拆分而言,搭档在哪里? 罗宾在哪里?



放在小车里 罗宾在一辆杂物车里等着。

边车


  • Batman是您的客户必须尽快看到的所有视觉材料。 理想的瞬间。
  • Robin是所有逻辑和花哨的交互式功能,可能在第二秒才可用,但不是一开始就可用。

最好将其称为并行分支中存在代码分支的垂直代码拆分 ,而不是将常见的代码分支剪切 掉的水平代码拆分


某些地区 ,此三重奏被称为replace reducer或其他延迟加载Redux逻辑和副作用的方法。


其他一些国家 ,这被称为"3 Phased" code splitting


这只是关注点的另一种分离,仅适用于您可以推迟加载组件的某些部分而不是另一部分的情况。

第三阶段


使用React,GraphQL和Relay构建新的facebook.com中的图像,其中importForInteractionsimportAfter sidecar

而且有一个有趣的发现-虽然Batman对客户来说更有价值,但只要有客户看到的东西,他就始终处于健康状态……而Robin可能有点超重 ,并且需要更多字节生活。


结果-蝙蝠侠独自一人​​对客户来说是可以承受的-他以较低的成本提供了更多的价值。 你是我的英雄蝙蝠!


可以移动到边车上的东西:


  • 大多数useEffectcomponentDidMount和朋友。
  • 像所有模态效果一样 即focusscroll锁定。 您可能首先显示模态,然后使模 ,即“锁定”客户的注意力。
  • 表格 将所有逻辑和验证移至边车,并阻止表单提交,直到加载该逻辑。 客户可能会开始填写表格,而不知道这只是Batman
  • 一些动画。 就我而言,这是一个完整的react-spring
  • 一些视觉的东西。 类似于“ 自定义”滚动条 ,它可能会在一秒钟后显示精美的滚动条。

另外,请不要忘记-卸载到Sidecar的每段代码,也卸载了已删除的代码所使用的core-js poly-和ponyfills之类的东西。


代码拆分比今天的应用程序更智能。 我们必须意识到有两种代码可以拆分:1)视觉方面2)交互方面。 稍后可能会出现后者。 Sidecar可以无缝地拆分两个任务,从而使您感觉到所有内容的加载速度都更快 。 而且会的。


最早的代码拆分方法


虽然尚不清楚何时以及什么是sidecar ,但我将给出一个简单的解释:


Sidecar您的全部脚本 。 Sidecar是我们在今天得到的所有前端东西之前进行代码拆分的方式。

我说的是服务器端渲染( SSR )或只是纯HTML ,我们都习惯了昨天。 当包含HTML和逻辑的页面分别存在于可嵌入的外部脚本中(关注点分离)时, Sidecar使事情变得像以前一样容易。


我们有HTML,CSS,内联的脚本以及提取到.js文件中的其余脚本。


HTML + CSS + inlined-jsBatman ,而外部脚本是Robin ,并且该网站能够在没有Robin的情况下正常运行,并且说实话,在没有Batman的情况下(他将在双腿断断续续的情况下继续战斗)。 那只是昨天,许多“非现代而酷”的网站今天都一样。




如果您的应用程序支持SSR,请尝试禁用js并使其在没有它的情况下正常工作。 然后,很明显可以将什么移动到边车上。
如果您的应用程序是仅客户端SPA,请尝试想象一下,如果存在SSR,它将如何工作。


例如,用React编写的-theurge.com功能全面, 无需启用任何js

您可能需要将很多事情转移到边车上。 例如:


  • 评论。 只要可能需要更多的代码(包括WYSIWYG编辑器)(最初不需要),您就可以附带display注释的代码,但不answer 。 最好延迟评论框 ,甚至只是将代码隐藏在动画后面,而不是延迟整个页面。
  • 视频播放器。 投放没有“控件”的“视频”。 一秒钟后加载它们,他们的客户可能会尝试与之交互。
  • 图片库,如slick绘制它并不是什么大问题,但要赋予动画和管理难度就更大了。 很明显,什么可以转移到边车上。

只需考虑对您的应用程序必不可少的内容,而对于应用程序而言则不尽相同...

实施细节


(DI)组件代码拆分


sidecar的最简单形式很容易实现-只需将所有内容移动到子组件,您就可以使用“旧”方式对代码进行拆分。 这几乎是Smart和Dumb组件之间的分离,但是这次Smart并不是在说Dumb一个-恰恰相反。


 const SmartComponent = React.lazy( () => import('./SmartComponent')); class DumbComponent extends React.Component { render() { return ( <React.Fragment> <SmartComponent ref={this} /> // <-- move smart one inside <TheActualMarkup /> // <-- the "real" stuff is here </React.Fragment> } } 

这也需要将初始化代码移至哑巴代码,但您仍然可以对代码中最重的部分进行代码拆分。


您现在可以看到parallelvertical代码拆分模式吗?

使用Sidecar


我已经在这里提到过, 使用React,GraphQL和Relay构建New facebook.com的概念是loadAfterimportForInteractivity ,这与sidecar概念非常相似。


同时,我不建议您创建诸如useSidecar类的东西,只要您可能有意尝试在其中使用hooks ,但是以这种形式进行代码拆分会破坏钩子规则


请选择一种更具声明性的组件方式。 而且,您可以在SideCar组件内使用hooks


 const Controller = React.lazy( () => import('./Controller')); const DumbComponent = () => { const ref = useRef(); const state = useState(); return ( <> <Controller componentRef={ref} state={state} /> <TheRealStuff ref={ref} state={state[0]} /> </> ) } 

预取


别忘了-您可以使用加载优先级提示来预加载或预取sidecar ,并使它的运输更加透明和不可见。


重要事项-预取脚本将通过网络加载它,但除非实际需要,否则不执行(并花费CPU)。


固态继电器


常规代码拆分不同,SSR不需要采取特殊操作。 Sidecar可能不是SSR流程的一部分,并且在hydration步骤之前不需要。 可以“按设计”将其推迟。


因此-可以随时使用React.lazy (理想情况下没有 Suspense ,这里不需要任何故障回复(加载)指示符)或任何其他库,但如果没有SSR支持,最好在SSR过程中跳过 Sidecar块。


不良部位


但是这个想法有一些坏处


蝙蝠侠不是产品名称


虽然Batman / Robin Batman / Robin可能是一个好主意,而maincar本身就是该技术的完美搭配-这款主sidecar并没有“好”的名字。 没有主车这样的东西,显然BatmanLonely WolfSolitudeDriverSolo不会被用来命名非人车零件。


Facebook已经使用了displayinteractivity ,这对于我们所有人来说可能是最好的选择。


如果您对我有个好名字-请将其保留在评论中

摇树


捆绑器的角度来看,更多的是关注点的分离。 假设您有BatmanRobin 。 还有stuff.js


  • stuff.js
     export * from `./batman.js` export * from `./robin.js` 

然后,您可以尝试拆分组件代码以实现Sidecar


  • main.js


     import {batman} from './stuff.js' const Robin = React.lazy( () => import('./sidecar.js')); export const Component = () => ( <> <Robin /> // sidecar <Batman /> // main content </> ) 

  • sidecar.js


     // and sidecar.js... that's another chunk as long as we `import` it import {robin} from './stuff.js' ..... 


简而言之-上面的代码可以工作,但不能做“工作”。


  • 如果您仅使用stuff.js batman -摇树只会保留它。
  • 如果您仅使用stuff.js robin -摇树只会保留它。
  • 但是,如果您同时使用这两者,即使它们使用的是不同的块-两者都将捆绑在第一次出现的stuff.js ,即主捆绑

树摇不是代码拆分友好的。 您必须按文件分开关注点。

取消导入


每个人都忘记的另一件事是javascript的成本。 在jQuery时代( jsonp有效载荷时代)非常普遍,它用于加载脚本(带有json有效载荷),获取有效载荷并删除脚本。


如今,我们都import脚本,即使不再需要它也将永远被导入。

就像我之前说过的-JS太多了,迟早要进行连续导航,您将全部加载。 我们应该找到一种方法,取消导入不再需要的块,清除所有内部缓存并释放内存以使Web更加可靠,并且不使用内存不足异常破坏应用程序。


un-import的能力(webpack 可以做到也许是我们坚持使用基于组件的 API的原因之一,只要它使我们能够处理unmount


到目前为止,ESM模块标准与此类内容无关,也与缓存控制无关,也没有与导入操作相反的内容。


创建支持sidecar的库


到目前为止,只有一种方法可以创建启用了sidecar库:


  • 将您的组件分成几部分
  • 通过index公开main部分和connected部分(不破坏API)
  • 通过单独的入口点暴露sidecar
  • 在目标代码中-导入main零件和sidecar -摇树应该切断connected零件。

这个时间树摇动应该正常工作,唯一的问题-是如何命名main部分。


  • main.js

 export const Main = ({sidecar, ...props}) => ( <div> {sidecar} .... </div> ); 

  • connected.js

 import Main from './Component'; import Sidecar from './Sidecar'; export const Connected = props => ( <Main sidecar={<Sidecar />} {...props} /> ); 

  • index.js

 export * from './Main'; export * from './Connected'; 

  • sidecar.js

 import * from './Sidecar'; 

简而言之,可以通过较小的比较来表示更改


 //your app BEFORE import {Connected} from 'library'; // // ------------------------- //your app AFTER, compare this core to `connected.js` import {Main} from 'library'; const Sidecar = React.lazy(import( () => import('library/sidecar'))); // ^ all the difference ^ export SideConnected = props => ( <Main sidecar={<Sidecar />} {...props} /> ); // ^ you will load only Main, Sidecar will arrive later. 

理论上, dynamic import可以在node_modules内部使用,从而使组装过程更加透明。


无论如何-它只不过是children / slot模式,在React中很常见。

未来


Facebook证明了这个想法是正确的。 如果您还没有看过该视频,请立即进行操作。 我只是从不同角度解释了相同的想法(并在F8会议召开的一周前开始撰写本文)。


现在,它需要对您的代码库进行一些代码更改。 它需要更明确地分离关注点才能真正将它们分离,并且让代码不是水平而是垂直拆分,以交付更少的代码以提供更大的用户体验。


除了老式SSR之外, Sidecar可能是处理BIG代码库的唯一方法。 当您拥有大量代码时,这是最后机会发布少量代码。


它可以使BIG应用程序更小,而SMALL应用程序更小。

10年前,中型网站已在300毫秒内“准备就绪”,并在几毫秒后才真正准备就绪。 今天,秒数甚至超过10秒是常见数字。 真可惜


让我们暂停一下,思考-我们如何解决问题,并使UX再次出色...



总的来说


  • 组件代码拆分是功能最强大的工具,它使您能够完全拆分某些内容,但要付出一定的代价-您可能不会显示任何内容,除非是空白页或一段时间。 那是一个水平的分离。
  • 库代码拆分可能会在组件拆分不起作用时提供帮助。 那是一个水平的分离。
  • 卸载到Sidecar上的代码将使图片更完整,并可以使您提供更好的用户体验。 但也需要一些工程上的努力。 这是垂直分隔。

让我们来讨论一下


别说了 那么您尝试解决的问题呢?


好吧,那只是第一部分。 我们现在正处于最后阶段 ,写下该提议的第二部分还需要几个星期。 同时...上车!

Source: https://habr.com/ru/post/zh-CN450942/


All Articles