从 Loading 动画出发改进 React 应用的用户体验

July 03, 2020

在 Web 1.0 时代,用户在不同的页面之间跳转基本上都是通过全量刷新页面来实现的,这个时候 Web 开发者基本上不用自己添加 Loading 效果,浏览器会有自己的 Loading 状态。

不过进入 Web 2.0 时代就不一样了,因为这个时候 Web 开发者可以利用 Ajax 来实现页面的局部刷新,但是网站的用户通常是分布在不同的地理位置并且他们的网络环境差异也是非常大的,这就导致了同样的操作,有些用户可能马上可以看到结果反馈,有些用户就要等比较久的时间才能看到操作结果。

如果用户在页面上点击了一个按钮,但是等了很久页面都没有看到任何反馈,这个时候给用户的感觉就是页面卡死了,或者停止响应了,这种结果对用户的心理体验是非常不利的,所以这个时候就要加入一个 Loading 动画来即时的对用户操作进行响应,例如这种效果:

loading

通过增加 Loading 动画,可以避免让用户产生页面卡死或者没有响应的心理反应,不过 Loading 动画在某些场景下也会带来新的问题。

Loading 动画的问题

如前面所说,用户会分布在不同的地理位置,并且拥有不同的网络环境(比如 光纤、4G、3G),这就导致他们的网络延迟是截然不同的,如果一个网站部署在美国西海岸,在不考虑服务器响应速度的情况下,美国西海岸的用户从发起请求到收到响应大概需要 100ms 左右, 而中国用户访问这个网站,同样的请求因为网络延迟的关系就要 2s 左右,这个时间在恶劣的网络环境下会更久。

想象一个场景,现在网站上有一个文章标题列表,点击其中一篇文章的标题后会跳转到文章页面,按照上面所说,美国西海岸的用户点击之后只需 100ms 就可以看到新的内容,中国用户点击后至少需要 2s 以后才能看到新内容,如果在用户点击按钮之后立即显示 Loading 动画,对于那些网络延迟足够低的用户来说就会出现类似下面这样的场景:

flash-loading.gif

可以看到,Loading 动画一闪而过,给人的一个心理体验是非常不好的。

2021 更新,感受一下某国内热门 app 的 web 版

Loading 动画的展示时机

从实际的用户心理角度讲,打开一个页面如果三秒后还是 Loading 状态,这个时候用户就会产生不耐烦的感受,甚至一些用户在这种场景下可能会直接关闭页面而不会继续等待。

根据前面的情况,我们设定一个理想的 Loading 动画展示条件:

  • 如果在指定的时间内收到了服务器响应,那就不展示 Loading 动画,超过指定时间后才进行展示
  • 如果展示了 Loading 动画,那至少要展示足够长的时间,不能一闪而过

根据上面的条件,用 React 进行实现:

function Loading(props) {
  const { isLoading, children, delay, minDuration } = props
  const [visible, setVisible] = useState(false)
  const startTime = useRef(0)

  useEffect(() => {
    const remaining = minDuration - (Date.now() - startTime.current)
    const timeout = isLoading ? delay : remaining >= 0 ? remaining : 0

    const timer = setTimeout(() => {
      setVisible(isLoading)
      if (isLoading) {
        startTime.current = Date.now()
      } else {
        startTime.current = 0
      }
    }, timeout)

    return () => {
      clearTimeout(timer)
    }
  }, [isLoading])

  if (visible) {
    return <SkeletonScreen />
  }

  return children === undefined ? null : children
}

Loading.defaultProps = {
  // 用户触发异步操作后需要等 400ms 才会显示 Loading 动画
  delay: 400,
  // 如果展示了 Loading 动画,至少要展示 700ms
  minDuration: 700,
  // 异步操作是否正在进行中
  isLoading: false,
}

改进后的 Loading 效果:

可以看到改进之后,网络延迟足够低的情况下用户不会看到 Loading 动画,当网络延迟高的时候才会看到。

提升应用响应速度才是王中王

Loading 动画是把双刃剑,如果你的应用响应速度足够快,完全可以在大部分情况下不需要显示 Loading 动画。

本文从以下两个角度来说明如何提高 React 应用的响应速度:

1. 尽可能早的开始加载数据和代码

在 React 应用里常见的请求数据方式是这样实现的:

// pageA.jsx (这个文件会被异步加载)
function PageA(props) {
  const [data, setData] = useState(null)

  useEffect(() => {
    requestArticleData(props.id).then((res) => setData(res.data))
  }, [props.id])

  return <div>{data ? <Article {...data} /> : null}</div>
}

这种方式的问题在于,它需要等到 PageA.jsx 这个文件加载完成,并执行完上面的代码,等 React 组件挂载之后才会开始请求这个页面需要的数据,它是这样一个串行的过程:

  1. 用户请求切换到 PageA
  2. 开始下载 PageA 的代码
  3. 执行 PageA 的代码
  4. 开始请求数据
  5. 拿到数据,显示页面

有一种更好一点的方法是这样的:

  1. 用户请求切换到 PageA
  2. 开始下载 PageA 的代码
  3. 开始请求数据
  4. 执行 PageA 的代码,显示页面

要实现提前加载页面需要的数据,可以把加载数据的逻辑提取出来放在一个静态方法中,这样在用户请求切换到 PageA 的时候就可以通过这个静态方法来提前加载这个页面需要的数据,这种方法在 Next.js 这类框架中被广泛使用。

function PageA(props) {
  return <div>{props.data ? <Article {...props.data} /> : null}</div>
}

PageA.getInitialProps = async ({ query }) => {
  const data = await requestArticleData(query.id).then((res) => res.data)
  return {
    data,
  }
}

2. 合理使用缓存策略

在一些实际场景中,数据变化的频率是非常低的,这种情况在第一次通过网络请求到数据后可以缓存起来,后续再次访问这个资源的时候可以直接从缓存读取。

但是一旦使用了缓存,就要考虑很多额外的问题,比如缓存的 key、时效性,好在现在很多工具库(react-query、urql、react-apollo)都内置了一些开箱即用的缓存策略:

  1. cache-and-network (先返回缓存内容,再发起网络请求更新这个缓存,最后返回更新后的缓存)
  2. network-and-cache (优先使用网络请求,断网情况下使用缓存)
  3. network-only (只使用网络请求,不缓存内容)

具体使用什么策略需要根据具体的场景进行取舍,除了上面的这些工具库外,还可以看情况使用 Service Wroker,它也有类似上面这些缓存策略。

React Suspense & useTransition

React 一直在致力于打造更好的用户体验,在开启 React 并发模式之后可以通过 Suspense 和 useTransition 更简单直观的来处理异步状态,不过截止目前相关 API 还处于实验性阶段,如果你感兴趣可以查看 React 官方提供的文档了解更多信息 。

相关资源:

如果你喜欢我的内容,请考虑请我喝杯咖啡☕吧,非常感谢🥰 。

If you like my contents, please support me via BuyMeCoffee, Thanks a lot.