likes
comments
collection
share

白屏根因分析&检测

作者站长头像
站长
· 阅读数 5

先有问题再有答案

  1. 白屏现象的本质是什么
  2. 有什么危害
  3. 白屏是如何产生的
  4. 如何监控到白屏
  5. 白屏如何修复

白屏的影响

  1. 如果线上发生白屏故障 页面pv uv 点击量 业务交易量等各种指标会瞬间迭0 业务系统全面瘫痪 毫无疑问这个是p0故障...
  2. 品牌形象严重受损:频繁或长时间的白屏问题可能影响用户对品牌的整体看法。它可能被视为技术不可靠或服务不专业的标志,对品牌形象和声誉造成长期损害。

关键链路

首先看下用户访问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不存在发生渲染报错。如果报错发生在根组件 则会导致页面白屏 如果报错发生在页面的子组件 则会导致子组件无法渲染 业务部分功能不可用

白屏的处理

  1. 网络层面的问题一般与业务无关 这个环节我们需要做的是完善监控和报警 可以及时发现异常
  2. 解析的兼容性问题 我们需要统一业务的目标版本,对目标环境通过babel做好polyfill.
  3. 执行时做好代码的cr和风险评估 在关键节点做好异常兜底。
  4. 渲染环节要充分利用好框架的errorboundry能力,渲染兜底页面同时上报错误栈。

白屏检测方案:

监控异常

这个是利用onerror的机制 监听页面内的关键资源是否发生了加载异常。或者监控js运行报错是否会导致页面崩溃....

dom树检测

  • 根节点判断 通过在页面底部插入一个脚本读取dom节点,判断dom是否发生变化,因为一般首屏的script标签是顺序同步执行的,在执行最后的脚本时,前面的基础js和业务js应该都已经下载解析执行完成了dom也已经发生了变化,所以这个阶段如果dom依然和初始化是一致的说明发生了异常,页面白屏了

  • 采样对比 白屏根因分析&检测

框架兜底

正如前面分析的一样 在渲染阶段 白屏本质是由于错误 导致框架不知道怎么渲染,所以干脆就不渲染。因此我们要充分利用好框架给我们提供的能力,毕竟在这个spa框架下 我们已经将dom托管给了vue/react了。 下面是利用vue的error-boundary实现的能力:

<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>