通过切割代码和预加载来提高页面加载速度
AwesomeDevin opened this issue · comments
预加载的好与不好
预加载意味着会发更多的请求,并且很可能用户最终也不会使用,这些流量会造成更大的服务器压力,换句话就是你会为此花更多钱。
好处就是用户体验会好点,对于网速很快的用户来说提升只是一点点,对于网速稍慢的用户来说提升会更明显。
参考资料
- Lazy loading (and preloading) components in React 16.6 | by Rodrigo Pombo | HackerNoon.com | Medium
- React Lazy: a take on preloading views - Maxime Heckel's Blog
关于 React.lazy 和动态 import 的一些测试
参考 Lazy loading (and preloading) components in React 16.6 | by Rodrigo Pombo | HackerNoon.com | Medium 做了一些测试。
不做动态 import
import { useState } from 'react'
// 这种是把 Desc 和当前页面其它代码打包到一个文件了
// 访问这个页面时就会下载这个文件然后显示界面
// 点击按钮后 Desc 会马上显示
import Desc from './Desc'
function App() {
const [showDesc, setShowDesc] = useState(false)
return (
<div>
<button onClick={() => setShowDesc(true)}>显示描述</button>
{showDesc && <Desc />}
</div>
)
}
export default App
加上动态 import
import { useState, lazy, Suspense } from 'react'
// 这种是把 Desc 单独打包了,但是在点击按钮的时候才会去下载文件
// 所以会先显示 loading... 然后文件下载完后显示 Desc
const Desc = lazy(() => import('./Desc'))
function App() {
const [showDesc, setShowDesc] = useState(false)
return (
<div>
<button onClick={() => setShowDesc(true)}>显示描述</button>
<Suspense fallback={<div>loading...</div>}>
{showDesc && <Desc />}
</Suspense>
</div>
)
}
export default App
加上动态 import 并提前下载文件
import { useState, lazy, Suspense } from 'react'
// 会在加载这个页面时就去下载 Desc 文件
// 当点击按钮的时候一般 Desc 文件已经下载完了,所以会直接显示
const descPromise = import('./Desc')
const Desc = lazy(() => descPromise)
function App() {
const [showDesc, setShowDesc] = useState(false)
return (
<div>
<button onClick={() => setShowDesc(true)}>显示描述</button>
<Suspense fallback={<div>loading...</div>}>
{showDesc && <Desc />}
</Suspense>
</div>
)
}
export default App
加上动态 import 并在鼠标 hover 时下载文件
import { useState, lazy, Suspense } from 'react'
const importDesc = () => import('./Desc')
const Desc = lazy(importDesc)
function App() {
const [showDesc, setShowDesc] = useState(false)
// 在鼠标移入时再去下载 Desc
const onMouseEnter = () => {
importDesc()
}
return (
<div>
<button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
显示描述
</button>
<Suspense fallback={<div>loading...</div>}>
{showDesc && <Desc />}
</Suspense>
</div>
)
}
export default App
封装一下,方便应用到其它组件
import { useState, lazy, Suspense } from 'react'
function lazyWithPreload(importFunc) {
const Component = lazy(importFunc)
// 加上一个 preload 属性,方便调用
Component.preload = importFunc
return Component
}
const Desc = lazyWithPreload(() => import('./Desc'))
function App() {
const [showDesc, setShowDesc] = useState(false)
// 在鼠标移入时再去下载 Desc
const onMouseEnter = () => {
Desc.preload()
}
return (
<div>
<button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
显示描述
</button>
<Suspense fallback={<div>loading...</div>}>
{showDesc && <Desc />}
</Suspense>
</div>
)
}
export default App
如果动态 import 的组件 A 里面还动态 import 了其它的组件 B
这种情况的话,是会在需要展示组件 B 的时候才去下载组件 B 的代码,因为你在鼠标移上去的时候只预加载了组件 A 。
// App.js
import { useState, Suspense } from 'react'
import lazyWithPreload from './lazyWithPreload'
const Desc = lazyWithPreload(() => import('./Desc'))
function App() {
const [showDesc, setShowDesc] = useState(false)
// 在鼠标移入时再去下载 Desc
const onMouseEnter = () => {
Desc.preload()
}
return (
<div>
<button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
显示描述
</button>
<Suspense fallback={<div>loading...</div>}>
{showDesc && <Desc />}
</Suspense>
</div>
)
}
export default App
// Desc.js
import { Suspense } from 'react'
import lazyWithPreload from './lazyWithPreload'
const SubDesc = lazyWithPreload(() => import('./SubDesc'))
function Desc() {
return (
<div>
<div>这是一段描述,假装这是一个很复杂的组件。</div>
{/* 这里会先显示 loading 然后再显示 SubDesc 内容 */}
<Suspense fallback={<div>loading...</div>}>
<SubDesc />
</Suspense>
</div>
)
}
export default Desc
直接使用 loadable-components 库来做预加载
react-router 推荐的 code splitting 库是 loadable-components ,这个库是支持 预加载 功能的。
import { useState } from 'react'
import loadable from '@loadable/component'
const Desc = loadable(() => import('./Desc'), {
fallback: <div>loading...</div>,
})
function App() {
const [showDesc, setShowDesc] = useState(false)
// 在鼠标移入时再去下载 Desc
const onMouseEnter = () => {
Desc.preload()
}
return (
<div>
<button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
显示描述
</button>
{showDesc && <Desc />}
</div>
)
}
export default App
按路由代码后能做预加载
上面说的都是页面中某个次要内容的代码分割和预加载,下面来进入正题,按路由切割代码后,能在当前路由预加载其它路由的代码吗?
自己封装一下 Link
我们可以把 react-router-dom 库的 Link 组件再封装一层来实现预加载。
把路径以及对应的组件定义为数组,方便我们封装的 LinkWithPreload 去遍历数组找到组件,然后去执行组件的 preload 就好了。
// App.js
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import loadable from '@loadable/component'
export const routes = [
{ path: '/', Component: loadable(() => import('./List')) },
{ path: '/detail/:id', Component: loadable(() => import('./Detail')) },
]
function App() {
return (
<Router>
<Switch>
{routes.map((item) => {
const { path, Component } = item
return (
<Route key={path} exact path={path}>
<Component />
</Route>
)
})}
</Switch>
</Router>
)
}
export default App
// LinkWithPreload.js
import { Link, matchPath } from 'react-router-dom'
import { routes } from './App'
function LinkWithPreload(props) {
const { to } = props
const onMouseEnter = () => {
const find = routes.find((item) => {
const { path } = item
const match = matchPath(to, {
path,
exact: true,
})
return Boolean(match)
})
if (find) {
find.Component.preload()
}
}
return <Link {...props} onMouseEnter={onMouseEnter} />
}
export default LinkWithPreload
在需要使用 Link 的地方就 <LinkWithPreload to='/xxx'>
查看</LinkWithPreload>
就可以了。
这里是鼠标移上去的时候预加载,如果你想,也可以改为使用 Intersection Observer ,判断 Link 组件进入可见区域时就预加载。
在什么时候进行预加载也是一种权衡,尽早预加载可以保证跳转页面的时候资源已经加载好了,但是会不可避免造成一些不必要的加载,因为你不知道用户会访问哪些页面。(当然如果你想你可以结合统计工具的数据,只对用户经常访问的页面做预加载来增加命中率 hhh)
直接使用 quicklink 库
https://github.com/GoogleChromeLabs/quicklink
它是监听的 Link 进入可视区域就进行预加载。
在 create-react-app 中使用:https://github.com/GoogleChromeLabs/quicklink/blob/master/demos/spa/README.md
使用这个库会需要配置 webpack-route-manifest 插件,这个插件会生成下面这个东西,然后就可以根据路由去预加载了。
quicklink 的相关实现见 https://github.com/GoogleChromeLabs/quicklink/blob/master/src/react-chunks.js#L61 和 https://github.com/GoogleChromeLabs/quicklink/blob/master/src/index.mjs#L60 。
它是等路由组件进入可视区域后,然后拿到路由组件中所有 a 标签,然后再对应去做预加载。
timeoutFn(() => {
// Find all links & Connect them to IO if allowed
(options.el || document).querySelectorAll('a').forEach(link => {
// If the anchor matches a permitted origin
// ~> A `[]` or `true` means everything is allowed
if (!allowed.length || allowed.includes(link.hostname)) {
// If there are any filters, the link must not match any of them
isIgnored(link, ignores) || observer.observe(link);
}
});
}, {
timeout: options.timeout || 2000,
});
总结
为了减少加载一个页面时需要下载的代码,我们可以:
按路由切割代码,并预加载其它路由代码(Link hover 时或者进入可视区域时),这样跳转时下个页面加载会更快;
对弹窗、Tab 等当前不需要展示或者低优先级内容做代码切割,并预加载。
代码切割是为了减少必要代码的体积,预加载是为了低优先级组件代码在需要时也能尽快展示。
preload、prefetch、动态 import 区别
preload 和 prefetch 是 HTML link 标签的一个用法,用于提示浏览器去提前下载资源。preload 是希望提前下载当前页面的资源。prefetch 是希望提前下载其它页面的资源。
动态 import 是 JS 的一个语法,Webpack 打包时会把动态 import 的部分打包为单独的文件。可以用于实现按路由切割代码,或者把弹窗等低优先级界面代码从主界面代码切割出去,这样来加快主界面的加载速度。
当你希望预加载资源时,是使用 link 的 prefetch 还是说动态 import ,其实结果都是一样的,可以结合项目用的库来看怎么实现简单怎么来。