避免异步加载带来的布局偏移

700 阅读2分钟

场景

这种场景十分罕见,不得不吐槽下产品的设计。

当页面异步请求没完成时,用户已经执行了锚点定位,并定位到了没完成请求的内容下方。显而易见地请求完成,一顿 Layout Shift 下来,用户找不着北。

分析

如果一个功能开发困难,那设计一定存在某种不合理

来看下 Google Developers 对于 CLS 的优化建议

对于大多数网站来说,您可以通过遵循一些指导原则来避免所有的意外布局偏移:

  • 始终在您的图像和视频元素上包含尺寸属性,或者通过使用CSS 长宽比容器之类的方式预留所需的空间。这种方法可以确保浏览器能够在加载图像期间在文档中分配正确的空间大小。请注意,您还可以使用unsized-media 功能策略在支持功能策略的浏览器中强制执行此行为。
  • 除非是对用户交互做出响应,否则切勿在现有内容的上方插入内容。这样能够确保发生的任何布局偏移都在预期之内。
  • 首选转换动画,而不是触发布局偏移的属性动画。动画过渡的目标是提供状态与状态之间的上下文连续性。

遇到这个问题推荐找产品讨论,是否真的需要为了这个功能去牺牲用户的视觉稳定性,战败了就往下看吧

推荐方案

解决方案优先选择

  1. Server Side Render
  2. 并发请求,利用 promiss.all 统一处理渲染
  3. 顺序请求

兜底方案

布局偏移如果已经不可避免,那么只能从调整用户视窗来稳定视觉焦点。

最直观的方法:当用户处于页面某一位置,如此时在视窗上方加入元素,则调整视窗位置到 原位置 + 偏移量

拆分出三个功能实现点

  • 偏移量

偏移量最好计算,直接给方法

// 这里假定 margin 是整数
const getDimension = (element) => {
  const height = element.offsetHeight
  const margin = parseInt(document.defaultView.getComputedStyle(element, '').getPropertyValue('margin-top')) + parseInt(document.defaultView.getComputedStyle(element, '').getPropertyValue('margin-bottom'))
  return height + margin
}
  • 新增元素相对视窗位置
const isAbove = (ele) => {
  if(!ele) return false
  return ele.getBoundingClientRect().top > 0
}

此处给出最简方法,只能判断元素顶部相对当前位置的状态。

还可以根据需求获取元素整体是否在视窗内ele.offsetTop < window.scrollY < ele.offsetTop + ele.offsetHeight

  • 找到元素被添加的时机
    const mutationCallback = function(mutationsList, observer) {
      for (let mutation of mutationsList) {
        if (!mutation.target) continue
        if (mutation.target.hasAttribute('attr-parent')) {
          const addedNode = mutation.addedNode && mutation.addedNodes[0]
          if (isAbove(addedNode)) {
            const offset = getDimension(addedNode)
            window.scrollTo({ top: window.scrollY + offset, behavior: 'auto' })
          }
        }
      }
    }
    this.observer = new MutationObserver(mutationCallback)
    const config = { childList: true, subtree: true }
    this.observer.observe(rootElement, config)

其中 attr-parent 被我添加到需要观测的元素父级 例如

<div attr-parent='true'>
    <TableA>
    <TableB>
    <TableC>
</div>
<div attr-parent='true'>
    <ChartA>
    <ChartB>
</div>

这样就对三个Table两个Chart完成了偏移观测