一文搞懂页面白屏检测

3,756 阅读9分钟

image.png

前言

前端页面白屏,是影响用户浏览体验的常见问题之一。它会导致页面长时间空白。尤其是对于SPA项目。

什么是页面白屏

页面白屏指页面在加载过程中长时间无法正常展示内容,用户处于等待状态的现象。 通常表现为:

  1. 页面空白,没有任何内容
  2. 页面仅显示背景色,没有文本、图片、页面元件等内容 
  3. 页面处于加载中状态,但长时间无法完成加载 
  4. etc.

页面白屏的主要原因

  1. 资源加载失败页面依赖的关键资源(CSS、JS、图片等)加载失败,导致页面无法正常渲染。
//例如我们在React项目中引入了不存在的css资源,导致资源加载失败,就会造成页面无法正常渲染,导致白屏。
//这是React 会在加载页面资源阶段就渲染 DOM 节点,但如果所依赖的资源(如图片)加载失败,则不会进行任何更新,导致用户长时间面对一个空白页面,产生白屏现象。
import './demo.css'
function App() {
  return (
    <div className="App">
     app
    </div>
  )
}
export default App

  1. 资源加载延迟资源加载延迟(或阻塞),导致页面长时间等待资源加载完成,出现空白。
//例如我们在这里,App 组件在挂载后会使用 setTimeout 模拟资源加载延迟 8 秒。  
//在资源加载完成前,imageURL 为空,因此不会渲染 <img> 标签。
//这就导致了用户在加载数秒内只看到一个空白页面(除非有其他内容),产生长时间空白导致的白屏现象。

import { useState, useEffect } from 'react'
function App() {
  const [imageURL, setImageURL] = useState(null)
  useEffect(() => {
    setTimeout(() => {
      setImageURL('../test.jpg')
    }, 8000)  // 模拟资源加载 8 秒
  }, [])
  return (
    <div className="App">
      {imageURL && <img src={imageURL} alt="logo" />}
    </div>
  )
}
export default App

对于上述这种情况,我们可以采用如下方式优化:

  • 加一个Loading指示器
{imageURL ? <img src={imageURL} alt="logo" /> : <Loading />}  
  • 等待资源加载完成后再渲染页面
//不在渲染时导入资源,而是在 useEffect 中导入,等待资源加载完成后再进行页面渲染
useEffect(() => {
  const logo = await import('./logo.png') 
  setImageURL(logo)
}, [])
  • 骨架屏
  • ....
  1. JavaScript执行错误,导致页面功能无法正常工作,出现空白。
//比如我们这里arr其实并未定义,这样执行就会报错,导致页面白屏
function App() {
  return (
    <div className="App">
      {
        arr.map(item=>item*2)
     }
    </div>
  )
}
export default App
  1. 渲染时间过长 页面过于复杂,渲染时间超出预期,导致用户长时间处于等待状态。 
  2. 第三方问题第三方服务或资源出现问题,导致页面功能依赖无法正常工作,出现白屏。
//App 组件从第三方 API 获取数据并渲染。如果 API 服务出现问题导致数据获取失
//败,useEffect 将永远处于 pending 状态,页面将只渲染一个 <Loader />。
//这将导致用户长时间面对加载指示器,产生依赖第三方服务问题导致的白屏现象。
import { useState, useEffect } from 'react'
function App() {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch('http://xxxxxxxxx/data')
      .then(res => res.json())
      .then(setData)
  }, [])
  if (!data) return <Loader />
  
  return (
    <div className="App">
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </div>
  )
}

export default App

6. 其它异常 如网络异常、缓存问题、CDN问题等也会导致页面白屏。

所以,总结来说,页面白屏是指:由于资源加载问题、执行错误、渲染时间过长等技术异常,导致页面长时间无法正常展示与响应的现象。它严重影响用户体验,因此我们需要采取有效措施来避免与修复页面白屏问题,保证PAGE体验质量。

如何监测页面白屏

方案一:检测根节点是否渲染+onerror监听

原理很简单,在当前主流 SPA 框架下,DOM 一般挂载在一个根节点之下(比如 <div id="app"></div> ),发生白屏后通常是根节点下所有 DOM 被卸载。该方案就是通过监听全局的 onerror 事件,在异常发生时去检测根节点下是否挂载 DOM,若无则证明白屏。

这种方案,简单直接,直接检测根节点是否渲染完成即可。适用于SPA。SPA页面主要内容通过根节点下的组件渲染,所以监测根节点渲染情况可以判断SPA页面主要内容是否正常渲染。

这是简单明了且有效的方案。

//main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// JS 错误监听 
window.onerror = function (msg, url, lineNo, columnNo, error) {
  const rootElement = document.querySelector('#root')
  if (!rootElement.firstChild) {
    console.log('#root节点不存在内容,判断为白屏!')
  }
}
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

//App.jsx

import React from 'react';
function App() {
  return (
    <div>
      App,
      {
        app.map(i => i * 2)  //我们这里app未定义,执行报错导致白屏
      }
    </div>
  );
}
export default App;
 


方案二:Mutation Observer 监听 DOM 变化

    let timeoutId=null
    const observer = new MutationObserver(callback);
    function callback(mutationsList, observer) {
      // 有 DOM 变化,说明页面还没有白屏,重置一个定时器
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        // 一段时间内没有任何 DOM 变化,说明页面已经白屏
        console.log('页面白屏');
      }, 3000);
    }
    const targetNode = document.body;
    const config = {
      childList: true,
      attributes: true,
      subtree: true,
      characterData: true,
    };
    observer.observe(targetNode, config);

使用 Mutation Observer 来监听 DOM 变化,从而判断页面是否白屏。需要注意的是,判断页面是否白屏的阈值时间应该根据页面的实际情况来确定,如果设置时间太短可能会误判,设置时间太长可能会影响页面性能。

同时如果用户长时间未操作DOM,Mutation Observer 监听到一定时间内没有 DOM 变化,就可能会误判为页面白屏。

实际应用中,可以通过一些手段来增加 DOM 的变化,从而避免误判。比如,在页面初始化时,可以在页面中插入一些隐藏的元素,然后定时更新这些元素的样式或内容,从而让 Mutation Observer 监听到 DOM 的变化。另外,一些自动化的数据推送、广告展示等行为也会引起 DOM 的变化,这些行为也可以被 Mutation Observer 监听到,从而避免误判。

相比于监测根节点是否渲染的方式,Mutation Observe有可判断渲染问题,不受资源加载影响,支持问题回溯等优势。

  • 问题回溯

Mutation Record 提供具体的 DOM 变更记录,可以支持白屏问题的回溯与定位。而检测根节点渲染无法提供问题的详细诊断信息。例如,如果一段时间内 DOM 变化只有删除元素操作,几乎没有新增或更新操作,可以判断可能存在删除逻辑错误导致白屏,这可以作为问题回溯的参考信息。 那如何利用Mutation Observe进行问题回溯呢?

   const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        switch (mutation.type) {
          case "childList":
            mutation.addedNodes.forEach(node => {
              console.log(node.nodeName)  // 新增节点名
            })
            mutation.removedNodes.forEach(node => {
              console.log(node.nodeName)  // 移除节点名
            })
            break
          case "attributes":
            console.log(mutation.attributeName, mutation.oldValue, mutation.newValue)
            // 打印出变化的属性名与新旧值
            break
        }
      })
    })
   observer.observe(document, {
      childList: true,
      attributes: true,
      subtree: true
    })
  • 可判断渲染问题

通过监听 DOM 添加、移除、更新等变化情况,可以判断 DOM 渲染是否正常进行,是否出现长时间不变化的情况。
而检测根节点渲染只能判断根节点是否已渲染完成,无法判断渲染过程中是否出现问题。

// Mutation Observer 
let lastTime 
const observer = new MutationObserver(mutations => {
  if (!lastTime) lastTime = Date.now()
  else if (Date.now() - lastTime > 1000) {
    // 1 秒内无任何 DOM 变化,判断为渲染逻辑错误导致白屏
  }
})
observer.observe(document, { 
  childList: true, 
  attributes: true 
})

// 检测根节点渲染
let root = document.querySelector('#root')
setInterval(() => {
  if (root.children.length > 0) {
    // 根节点有子元素,判断渲染完成,无法判断渲染过程问题
  }  
}, 200)
  • 不受资源加载影响

Mutation Observer 判断白屏不依赖资源是否加载完成,可以判断出资源加载失败导致的白屏。 
而检测根节点渲染需要资源正常加载完成,无法判断资源失败导致根节点无法渲染的白屏。例如,如果页面 CSS 文件加载失败,导致页面无法渲染,Mutation Observer 可以判断为白屏,而检测根节点渲染无法判断此问题。

这是因为,通过监听 DOM 变更判断页面是否正常工作,即使资源加载失败,也可以发现页面长时间没有渲染出新内容的问题。而检测根节点渲染的方式,无法判断资源加载失败的情况。同时它可以判断出某类型节点(如 img)长时间没有被添加的情况。例如,如果页面长时间没有新增 img 节点,这可能意味着图片资源加载失败,导致 img 节点无法渲染。而检测根节点渲染同样无法判断具体哪些资源加载失败。

let imgCount = 0
let time = 0 
const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    if (mutation.addedNodes.find(n => n.nodeName === 'IMG')) {
      imgCount++ // 有 img 节点新增,重置时间
      time = 0 
    }
  })
})
observer.observe(document, { childList: true, subtree: true })
setInterval(() => {
  time += 200
  在回调中判断,如果一定时间(如 3 秒)内 imgCount 数为 0,则判断图片资源加载失败
  if (time > 3000 && imgCount === 0) {
    // 3 秒内无 img 节点新增,判断图片资源加载失败
  } 
}, 200)

同理,可以判断其他资源(如 JS、CSS 文件)加载失败。

方案三:关键点采样对比

所谓关键点采样就是在我们的屏幕中,随机取几个固定的点,利用document.elementsFromPoint(x,y)该函数返还在特定坐标点下的 HTML 元素数组。

关键点的选取我们一般采用:垂直选取

image.png

假设,要在X,Y轴上各埋9个点,每个点的距离相等,那么X轴上的点坐标就是(i/10 * 屏幕的宽度,1/2 * 屏幕的高度, i 代表第几个点。那么Y轴上的坐标就是(1/2 * 屏幕的宽度,i / 10 * 屏幕的高度)。

除了垂直选取,还有交叉选取,以及垂直交叉选取

交叉选取:

image.png

垂直交叉选取:

image.png

语法:

var elements = document.elementsFromPoint(x, y); 
// x:坐标点的水平坐标值,y:坐标点的垂向坐标值
//返回值是一个包含[`element`](https://developer.mozilla.org/zh-CN/docs/Web/API/Element) 对象的数组。

具体使用:


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .content{
      width: 100px;
      height: 100px;
    }
  </style>
</head>
<body>
  <div class="content"></div>
  <script>
    let ele=document.elementsFromPoint(50,50)
    console.log('ele',ele[0]);
  </script>
</body>
</html>

image.png

采样对比代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div class="main"></div>
  <script>
    function onload() {
      if (document.readyState === 'complete') {
        whiteScreen()
      } else {
        window.addEventListener('load', whiteScreen)
      }
    }
    let wrapperElements = ['html', 'body', '.content'] //首先定义容器列表
    let emptyPoints = 0 //空白点数量
    function getSelector(element) { //获取节点的容器
      if (element.id) {
        return '#' + element.id
      } else if (element.className) {  //content main==> .content.main  主要为了处理类名是多个的情况
        return '.' + element.className.split(' ').filter(item => !!item).join('.')
      } else {
        return element.nodeName.toLowerCase()
      }
    }
    function isWrapper(element) { //判断关键点是否在wrapperElements定义的容器内
      let selector = getSelector(element)
      if (wrapperElements.indexOf(selector) != -1) {
        emptyPoints++ //如果采样的关键点是在wrapperElements容器内,则说明此关键点是空白点,则数量加1
      }
    }
    function whiteScreen() {
      for (let i = 1; i <= 9; i++) {
        let xElement = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)//在x轴方向上,取10个点
        let yElement = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)//在y轴方向上,取10个点
        isWrapper(xElement[0])
        isWrapper(yElement[0])
      }
      if (emptyPoints != 18) {//如果18个点不都是空白点,则说明页面正常显示
        clearInterval(window.loopFun)
        window.loopFun = null
      } else {
         console.log('页面白屏了');
        if (!window.loopFun) {
          loop()
        }
      }
    }
    window.loopFun = null
    function loop() {
      if (window.loopFun) return;
      window.loopFun = setInterval(() => {
        emptyPoints=0
        whiteScreen()
      }, 2000)
    }
    onload()
  </script>
    <script>
      let content = document.querySelector('.main')
      setTimeout(() => {
        content.style.width = '500px'
        content.style.height = '500px'
        content.style.backgroundColor = 'red'
      }, 4000);
    </script>
</body>

</html>

总结:

这是我自己的第一篇文章,对于页面白屏的一些看法,希望大佬们多多指正,写的不好见谅。

参考:

前端白屏的检测方案,让你知道自己的页面白了