前言
前端页面白屏,是影响用户浏览体验的常见问题之一。它会导致页面长时间空白。尤其是对于SPA项目。
什么是页面白屏
页面白屏指页面在加载过程中长时间无法正常展示内容,用户处于等待状态的现象。 通常表现为:
- 页面空白,没有任何内容
- 页面仅显示背景色,没有文本、图片、页面元件等内容
- 页面处于加载中状态,但长时间无法完成加载
- etc.
页面白屏的主要原因
- 资源加载失败页面依赖的关键资源(CSS、JS、图片等)加载失败,导致页面无法正常渲染。
//例如我们在React项目中引入了不存在的css资源,导致资源加载失败,就会造成页面无法正常渲染,导致白屏。
//这是React 会在加载页面资源阶段就渲染 DOM 节点,但如果所依赖的资源(如图片)加载失败,则不会进行任何更新,导致用户长时间面对一个空白页面,产生白屏现象。
import './demo.css'
function App() {
return (
<div className="App">
app
</div>
)
}
export default App
- 资源加载延迟资源加载延迟(或阻塞),导致页面长时间等待资源加载完成,出现空白。
//例如我们在这里,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)
}, [])
- 骨架屏
- ....
- JavaScript执行错误,导致页面功能无法正常工作,出现空白。
//比如我们这里arr其实并未定义,这样执行就会报错,导致页面白屏
function App() {
return (
<div className="App">
{
arr.map(item=>item*2)
}
</div>
)
}
export default App
- 渲染时间过长 页面过于复杂,渲染时间超出预期,导致用户长时间处于等待状态。
- 第三方问题第三方服务或资源出现问题,导致页面功能依赖无法正常工作,出现白屏。
//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 元素数组。
关键点的选取我们一般采用:垂直选取
假设,要在X,Y轴上各埋9个点,每个点的距离相等,那么X轴上的点坐标就是(i/10 * 屏幕的宽度,1/2 * 屏幕的高度, i 代表第几个点。那么Y轴上的坐标就是(1/2 * 屏幕的宽度,i / 10 * 屏幕的高度)。
除了垂直选取,还有交叉选取,以及垂直交叉选取。
交叉选取:
垂直交叉选取:
语法:
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>
采样对比代码:
<!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>
总结:
这是我自己的第一篇文章,对于页面白屏的一些看法,希望大佬们多多指正,写的不好见谅。
参考: