前言

在日常的前端开发中,白屏几乎是每个前端开发者都会遇到的问题。白屏问题严重影响了用户体验。当用户访问一个页面时,如果页面长时间处于白屏状态,用户可能会认为页面出现了问题,从而选择离开。这对于任何一个网站都是不利的,尤其是对于那些依赖用户流量的电商网站来说,白屏问题可能直接导致用户流失和收入下降。

许多研究都表明,用户最满意的打开网页时间,是在2秒以下。 用户能够忍受的最长等待时间的中位数,在6~8秒之间。 这就是说,8秒是一个临界值,如果你的网站打开速度在8秒以上,那么很可能,大部分访问者最终都会离你而去。

白屏的主要原因

白屏的主要原因可以从浏览器渲染的整个流程来分析,我们知道从一次请求到页面渲染大致会经过以下过程

用户输入URL -> DNS解析 -> 建立连接 -> 返回内容 -> 浏览器解析响应内容 -> 浏览器渲染

其中任何一环出现问题都有可能造成页面白屏,下面是一些最常见的白屏原因:

1. 资源加载失败页面依赖的关键资源(CSS、JS、图片等)加载失败,导致页面无法正常渲染。
//例如我们在React项目中引入了不存在的js资源或者资源加载失败,就会造成页面无法正常渲染,导致白屏。
import './index.js'
function App() {
  return (
    <div className="App">
     app
    </div>
  )
}
export default App

这种情况下我们可以在关键的接口和请求上增加响应监控。

2. 资源加载延迟资源加载延迟(或阻塞),导致页面长时间等待资源加载完成。出现空白。
//例如我们在这里,App 组件在挂载后会使用 setTimeout 模拟资源加载延迟 8 秒。  
import { useState, useEffect } from 'react'
import Preview from 'preview'
function App() {
  const [data, setData] = useState(null)
  useEffect(() => {
    setTimeout(() => {
      setData({ name: 'test' })
    }, 10000)  // 模拟资源加载 10000 秒
  }, [])
  return (
    <div className="App">
      {data && <Preview />}
    </div>
  )
}
export default App

这种情况可以前端做一些loading状态或者增加骨架屏,防止页面无内容造成用户困扰。

3. 代码执行中出现未被捕捉的错误,例如JavaScript执行错误,Promise错误等等。导致页面功能无法正常工作,出现空白。

这个错误也是前端最最最常见的一个错误,如果没有处理好异常情况导致抛出错误有没有被捕获,就会造成页面崩溃。

function App() {
  return (
    <div className="App">
      {
        JSON.parse(data.name) // 未对data.name判空导致parse解析失败
     	}
    </div>
  )
}
export default App
4. 浏览器兼容问题

不同的浏览器对于前端技术的支持程度不同,如果我们使用了浏览器不支持的语法或者CSS类型,可能导致某些浏览器无法正常显示页面。

function App() {
  return (
    <div className="App">
      {data?.name} // 例如浏览器不支持可选链式符就会报错, https://caniuse.com/?search=%3F.
    </div>
  )
}
export default App

除此之外,第三方服务或资源出现问题,浏览器缓存、CDN问题等也会导致页面白屏的问题。在实际开发中我们需要考虑到各种可能的情况逐步排查。

如何监测页面白屏

要检测白屏,主线的思路就是检测在特定时间特定的DOM是否存在于页面中。或者是通过页面截图并且识别图片是否包含内容等方式(代价太高)。下面就给大家分享几种常用的方式,最后也会给出一种最通用的方式。

1. 全局错误监听 + 判断对应节点是否存在

例如在React中我们可以使用ErrorBoundary实现由于代码执行错误导致白屏的检测步骤:

  1. 首先,创建一个名为 ErrorBoundary 的新组件。在这个组件中,我们需要定义一个名为 componentDidCatch 的生命周期方法,它将在子组件中捕获到错误时被调用。同时,我们需要在组件的状态中存储一个表示是否发生错误的变量。
import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // 在这里可以将错误记录到日志,或者发送给后端
    console.log(error, info);
    this.setState({ hasError: true });
    if (!document.getElementById('container').innerHTML) {
      console.log('页面白屏了');
    }
  }

  render() {
    // if (this.state.hasError) {
    //   // 如果发生错误,显示备用 UI
    //   return <h1>出错了,请刷新页面或联系客服。</h1>;
    // }

    // 如果没有发生错误,正常渲染子组件
    return this.props.children;
  }
}

export default ErrorBoundary;

  1. 接下来,将 ErrorBoundary 组件包裹在应用程序的根组件中,以便捕获整个应用程序的错误。
const App = () => {
  return <div>{a?.sjakd}</div>
}

import React from 'react';
import ReactDOM from 'react-dom';
import ErrorBoundary from './ErrorBoundary';

ReactDOM.render(
  <React.StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </React.StrictMode>,
  document.getElementById('root')
);

现在,如果应用程序中的任何子组件发生错误,ErrorBoundary 组件将捕获该错误,并判断对应的根节点是否函数对应的节点来实现白屏检测。

2. Mutation Observer 监听 DOM 变化

MutationObserver API 可以用于监测DOM树的变化。我们可以通过MutationObserver来监测页面的DOM变化,从而判断页面是否出现白屏现象。

class App extends React.Component {
  componentDidMount() {
    const observer = new MutationObserver((mutations, observer) => {
      const duration = Date.now() - observer.startTime;

      if (duration > 3000) {
        console.log('页面白屏时间超过3秒');
      }
    });

    observer.startTime = Date.now();
    observer.observe(document.body, { childList: true, subtree: true });
  }

  // ...
}

以React框架为例,我们可以在componentDidMount生命周期函数中使用MutationObserver API 获取相关信息。如果两次渲染时间过长说明出现了白屏的情况。

但是这种方法对下面这两种情况没有很好的解决办法

1)DOM根本就没有渲染

2)遇到有骨架屏的项目,若页面从始至终就没变化,一直显示骨架屏,这种情况 Mutation Observer 也束手无策

3. 关键点采样对比

所谓关键点采样就是在我们的屏幕中,随机取几个固定的点,利用document.elementsFromPoint(x,y)该函数返还在特定坐标点下的 HTML 元素数组。这也是准确率比较高的一种做法,目前主流的都是这种检测方法。具体实现如下:

1、页面中间取n个采样点,利用 elementsFromPoint api 获取该坐标点下的 HTML 元素。采样方法有垂直选取交叉选取,以及垂直交叉选取三种方法,对应的采样图片如下:

image-20240703153111902 image-20240703153128412 image-20240703153150175

2、定义属于容器元素的集合,如 ['html', 'body', '#app', '#root']

3、判断这n个采样点是否在该容器集合中。目的就是为了判断采样点有没有内容;如果没有内容,那么我们获取到的 dom 元素就是容器元素,若n个采样点都没有内容则可判定为白屏

4、若初次判断是白屏,开启轮询检测,来确保白屏检测结果的正确性,直到页面的正常渲染或者重试一定次数就关闭定时器

具体代码实现如下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<script>
  function whiteScreenMonitor(wrapperSelectors) {
  // 记录空白点数
  let emptyPoints = 0;
  let timer = null;
  const maxRetryTime = 5;
  let retryTime = 0;

  // 获取元素的选择器
  function getSelector(element) {
    if (element?.id) {
      return `#${element.id}`;
    } if (element?.className) {
      return (
        `.${
          element.className
            .split(' ')
            .filter(item => !!item)
            .join('.')}`
      );
    }
    return element?.nodeName.toLowerCase();
  }

  // 判断元素是否为包裹元素
  function isWrapper(element) {
    const selector = getSelector(element);
    if (wrapperSelectors.indexOf(selector) !== -1) {
      return true;
    }
  }

  function checkBlankScreen() {
    // 检查屏幕的点,分别为横向和纵向的点
    for (let i = 1; i <= 9; i++) {
      // 获取当前点的所有元素
      const xElements = document.elementsFromPoint(
        (window.innerWidth * i) / 10,
        window.innerHeight / 2,
      );
      const yElements = document.elementsFromPoint(
        window.innerWidth / 2,
        (window.innerHeight * i) / 10,
      );

      // 取第一个来判断
      emptyPoints += !!isWrapper(xElements[0]);
      emptyPoints += !!isWrapper(yElements[0]);
    }

    // 如果空白点数超过16个,表示屏幕为空白
    if (emptyPoints > 16) {
      console.log(`这是第 ${retryTime} 次检测,页面白屏了`);
      if (++retryTime > maxRetryTime) {
        console.log('页面白屏检测超过最大次数,可判定为白屏');
        // 这里可以做一些监控上报之类的事情
        clearTimeout(timer);
        return;
      }
      if (!timer) {
        timer = setInterval(() => {
          emptyPoints = 0;
          checkBlankScreen();
        }, 1000);
      }
    } else {
      clearTimeout(timer);
    }
  }
  window.addEventListener('load', checkBlankScreen);
}

// 开始检测白屏,如果是SDK则可以将这个当法导出
whiteScreenMonitor(['html', 'body', '#container', '.content', '#app', '#root']);
</script>
<body>
<div id="root"></div>
</body>
<script>
  setTimeout(() => {
    const content = document.createElement('div')
    content.style.width = '500px'
    content.style.height = '500px'
    content.style.backgroundColor = 'red'
    document.getElementById('root').appendChild(content) // 挂载
  }, 10000); // 模拟白屏的操作
</script>
</html>

这里使用初次检测 + 轮训防止误判的方式来进行白屏检测。关键内容在函数checkBlankScreen中取点并判断内容是容器节点的个数。

image-20240703155810525

使用了骨架屏的页面如何检测白屏

对于有骨架屏的页面,用户打开页面后,先看到骨架屏,然后再显示正常的页面,来提升用户体验;但如果页面从始至终都显示骨架屏,也算是白屏的一种

骨架屏示例:https://ant-design.antgroup.com/components/skeleton-cn

检测骨架屏的白屏的方式其实也很简单,我们可以稍微改改上面的方法即可:

  1. 传入骨架屏对应的容器给whiteScreenMonitor函数,这样检测的时候就可以将骨架屏的内容过滤掉。
  2. 改变检测白屏的方式,我们通过每次获取到的内容和第一次获取到的内容进行对比,如果每一次都相同说明是白屏的。但是这里就需要保证我们的白屏检测代码一定要是最先运行的,不然等我们所有的内容都渲染完了再检测,这是后内容也是没有改变的。

总结

以上就是常用的白屏检测方法了,相信看完大家以后对于如何检测项目中白屏有了自己的看法。

最后打个广告,我新开了个公众号,旨在将自己日常学习的内容进行沉淀。这个公众号会经常更新前端相关的技术文章,还请大家多多支持,点点关注💗。

image-20240703162104337
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐