先有问题再有答案
白屏现象的本质是什么
有什么危害
白屏是如何产生的
如何监控到白屏
白屏如何修复
白屏的影响
- 如果线上发生白屏故障 页面pv uv 点击量 业务交易量等各种指标会瞬间迭0 业务系统全面瘫痪 毫无疑问这个是p0故障...
- 品牌形象严重受损:频繁或长时间的白屏问题可能影响用户对品牌的整体看法。它可能被视为技术不可靠或服务不专业的标志,对品牌形象和声誉造成长期损害。
关键链路
首先看下用户访问H5页面的关键链路: 用户从点击入口到看到内容依次会经历上面的几个关键节点。
下载 (Loading):
这里主要是网络层面的事情例如dns,tcp,http握手等获取到html,js,css,图片,音视频的内容资源文件。
解析 (Parsing):
- HTML解析:浏览器开始解析HTML文档,构建DOM(文档对象模型)树。这个阶段是将标记语言转换成浏览器能理解和操作的结构化数据。
- CSS解析:同时,CSS文件被解析成CSSOM(CSS对象模型)。
- JS解析:JavaScript代码被解析并编译。浏览器会解析JavaScript脚本,可能暂停HTML的解析,这是因为JavaScript可能需要修改DOM结构。
执行(Execution)
执行已解析的JavaScript代码,JavaScript可能会操作DOM和CSSOM,注册事件处理函数,以及调用Web API等。
API请求(API Requests)
页面上的JavaScript往往会发起API请求,获取服务器端的数据。这些数据请求通常通过XMLHttpRequest或Fetch API进行。
渲染数据(Render Data)
使用从API获取的数据,JavaScript会更新DOM结构,如插入数据、修改元素等操作。DOM的更新可能导致页面布局的更改(重排)和元素外观的更改(重绘)
交互状态(Interactive)
此时页面已经完全加载,所有的事件监听器都已准备就绪,用户可以与页面进行交互,例如点击按钮、提交表单等。
白屏的根因
当上面的某个环节耗时较长或者发生异常阻塞后面的流程 导致用户看不到有意义的内容即发生白屏。所以白屏是渲染的过渡或者渲染异常的表现。
下载:
- 网络延迟或中断:网络问题可能导致资源文件未完全加载。
- 服务器故障:如果服务器遇到故障(例如,服务器宕机、配置错误等),可能无法正确响应请求。
- 资源找不到(404错误):请求的资源文件(如JavaScript或CSS文件)在服务器上不存在。
- cdn异常:使用的cdn服务不可用异常宕机 导致用户无法加载到资源
解析:
例如在低版本浏览器中使用es6+的语法, 引擎解析阶段无法识别一些符号 导致js解析异常 影响了后续流程, 引起页面白屏。
执行:
执行阶段因为开发编写的逻辑异常&没有做好兜底兼容导致白屏。例如首屏接口的处理没有try catch.
渲染:
一般UI框架(react/vue)都会有一些兼容处理避免因为渲染异常导致白屏,但是在读取一些动态数据 无法获取的时候,依然会引起渲染异常导致页面白屏。
例如我们在vue框架中,在模板中使用a.b.c取值时,可能会因为业务逻辑导致a||b不存在发生渲染报错。如果报错发生在根组件 则会导致页面白屏
如果报错发生在页面的子组件 则会导致子组件无法渲染 业务部分功能不可用
白屏的处理
- 网络层面的问题一般与业务无关 这个环节我们需要做的是完善监控和报警 可以及时发现异常
- 解析的兼容性问题 我们需要统一业务的目标版本,对目标环境通过babel做好polyfill.
- 执行时做好代码的cr和风险评估 在关键节点做好异常兜底。
- 渲染环节要充分利用好框架的errorboundry能力,渲染兜底页面同时上报错误栈。
白屏检测方案:
监控异常
这个是利用onerror的机制 监听页面内的关键资源是否发生了加载异常。或者监控js运行报错是否会导致页面崩溃....
dom树检测
-
根节点判断
通过在页面底部插入一个脚本读取dom节点,判断dom是否发生变化,因为一般首屏的script标签是顺序同步执行的,在执行最后的脚本时,前面的基础js和业务js应该都已经下载解析执行完成了dom也已经发生了变化,所以这个阶段如果dom依然和初始化是一致的说明发生了异常,页面白屏了
-
采样对比
原理就是从页面中取出一些关键dom或者按照一定的逻辑均分页面。然后轮训一定次数 判断获取的节点是否为容器节点或者判断获取的节点宽高是否为0.
H5的白屏检测方案实践
前端白屏的检测方案,让你知道自己的页面白了
框架兜底
正如前面分析的一样 在渲染阶段 白屏本质是由于错误 导致框架不知道怎么渲染,所以干脆就不渲染。因此我们要充分利用好框架给我们提供的能力,毕竟在这个spa框架下 我们已经将dom托管给了vue/react了。
下面是利用vue的error-boundary
实现的能力:
xml
<template>
<div :class="className">
<slot v-if="PageStatus.init === status"></slot>
<template v-if="PageStatus.error === status">
<slot v-if="showDefault" name="error"></slot>
<div v-if="!showDefault" class="content">
<p class="text">页面渲染异常</p>
<div class="btn" @click="onRefresh">刷新试试</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, onErrorCaptured, PropType, ref } from '@vue/composition-api';
import { IError, PageStatus } from './type';
const componentName = `vue-error-boundary`;
export default defineComponent({
name: componentName,
components: {},
props: {
/**
* targetId
* 被监控的目标组件name 输入空字符匹配所有
* 目标组件一般为业务根组件
*/
targetId: {
type: Array as PropType<Array<string>>,
default: [],
},
/**
* onRenderErrorTarget
* 监控的目标组件报错 目标组件一般为业务根组件
* !! 这里报错意味着业务已经完全不可用
*/
onRenderErrorTarget: {
type: Function as PropType<(params: IError) => void>,
default: () => ({}),
},
/**
* onRenderError
* 子组件有渲染报错
* !! 这里报错意味着业务部分不可用
*/
onRenderError: {
type: Function as PropType<(params: IError) => void>,
default: () => ({}),
},
/**
* onError
* 捕获全部异常 包括js运行异常 渲染异常 生命周期执行异常
* !! 通常用于上报雷达发送自定义埋点
* 可以收集到子组件的全部报错
*/
onError: {
type: Function as PropType<(params: IError) => void>,
default: () => ({}),
},
},
setup(p, ctx) {
const showDefault = ref(!!ctx.slots.error);
const status = ref(PageStatus.init);
const { onRenderError, targetId, onRenderErrorTarget, onError } = p;
onErrorCaptured((err: any, vm: any, info: string) => {
try {
const params = {
component: vm,
err,
type: info,
tag: vm.$vnode.tag,
propkeys: Object.keys(vm.$vnode.componentOptions?.propsData || {}),
instanceKeys: Object.keys(vm.$vnode.componentInstance || {}).filter(
item => item[0] !== '_' && item[0] !== '$',
),
};
if (info === 'render') {
if (targetId.some(item => `${vm.$vnode.tag}`.endsWith(`${item}`))) {
status.value = PageStatus.error;
onRenderErrorTarget(params);
} else {
onRenderError(params);
}
}
onError(params);
} catch (error) {
onError({
err: error
});
}
});
const onRefresh = () => {
location.reload();
};
return {
showDefault,
status,
PageStatus,
onRefresh,
className: componentName,
};
},
});
</script>