Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

通过切割代码和预加载来提高页面加载速度 #84

Open
AwesomeDevin opened this issue Mar 7, 2023 · 0 comments
Open

通过切割代码和预加载来提高页面加载速度 #84

AwesomeDevin opened this issue Mar 7, 2023 · 0 comments
Labels

Comments

@AwesomeDevin
Copy link
Owner

预加载的好与不好

预加载意味着会发更多的请求,并且很可能用户最终也不会使用,这些流量会造成更大的服务器压力,换句话就是你会为此花更多钱。

好处就是用户体验会好点,对于网速很快的用户来说提升只是一点点,对于网速稍慢的用户来说提升会更明显。

参考资料

  1. Lazy loading (and preloading) components in React 16.6 | by Rodrigo Pombo | HackerNoon.com | Medium
  2. 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 插件,这个插件会生成下面这个东西,然后就可以根据路由去预加载了。
image
quicklink 的相关实现见 https://github.com/GoogleChromeLabs/quicklink/blob/master/src/react-chunks.js#L61https://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 ,其实结果都是一样的,可以结合项目用的库来看怎么实现简单怎么来。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant