1. 页面白屏如何排查?
第一步:快速分类(30秒) "页面白屏主要有五种原因:JavaScript执行错误、资源加载失败、CSS样式问题、接口异常和浏览器兼容性。其中JavaScript错误最常见,特别是SPA应用中的未捕获异常。"
第二步:排查方法(1分钟) "我的排查步骤是:首先查看Console面板的错误信息,这能快速定位JS异常;然后检查Network面板确认资源加载状态;接着用Elements面板验证DOM和样式;移动端问题会用vConsole或真机调试。生产环境结合Sentry等监控系统分析。"
第三步:预防措施(30秒) "预防方面建立错误边界、资源容错机制、统一接口异常处理、兼容性检测,同时搭建监控告警体系。"
一、基础检测流程
1. 控制台检查(Console)
javascript
// 主动捕获全局错误(放在入口文件最前面)
window.addEventListener('error', function(event) {
console.error('全局捕获:', event.error);
// 可上报到监控系统
});
// 检查console是否有以下类型错误:
// - SyntaxError (语法错误)
// - TypeError (类型错误)
// - ReferenceError (引用错误)
// - 404资源加载失败
2. 网络请求检查(Network)
-
关键指标:
- HTML文档状态码(200/304/404/500)
- JS/CSS资源加载状态
- 接口请求是否阻塞渲染
-
检测示例:
ini
// 检查关键资源是否加载完成
const resourceCheck = () => {
const entries = performance.getEntriesByType('resource');
const criticalResources = entries.filter(entry =>
entry.initiatorType === 'script' ||
entry.initiatorType === 'css'
);
criticalResources.forEach(res => {
if(res.responseStatus >= 400) {
console.error(`资源加载失败: ${res.name}`, res);
}
});
};
window.addEventListener('load', resourceCheck);
二、深度检测方法
1. DOM渲染检测
scss
// 检测DOM树是否正常构建
function checkDOMReady() {
return new Promise((resolve) => {
const check = () => {
if(document.body && document.body.children.length > 0) {
resolve(true);
} else {
setTimeout(check, 50);
}
};
check();
});
}
// 使用示例
checkDOMReady().then((isReady) => {
if(!isReady) {
console.error('DOM渲染超时');
// 上报白屏信息
}
});
2. 框架特定检测
Vue应用检测:
javascript
// 在main.js中添加
new Vue({
render: h => h(App),
errorCaptured(err, vm, info) {
console.error('Vue组件错误:', err, info);
// 可上报错误
return false; // 阻止错误继续向上传播
}
}).$mount('#app');
// 检查根组件挂载
if(!document.querySelector('#app').__vue__) {
console.error('Vue根实例挂载失败');
}
React应用检测:
javascript
// Error Boundary组件
class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
console.error('React组件错误:', error, info);
// 上报错误
}
render() {
return this.props.children;
}
}
// 检查React根组件
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
三、性能相关检测
1. 长任务检测
javascript
// 检测阻塞渲染的长时间任务
const observer = new PerformanceObserver((list) => {
for(const entry of list.getEntries()) {
if(entry.duration > 50) { // 超过50ms的任务
console.warn('长任务影响渲染:', entry);
}
}
});
observer.observe({entryTypes: ["longtask"]});
2. 关键渲染路径监控
ini
// 使用Performance API监控关键时间点
const perfData = window.performance.timing;
const metrics = {
domReady: perfData.domComplete - perfData.domLoading,
loadTime: perfData.loadEventEnd - perfData.navigationStart
};
if(metrics.domReady > 3000) {
console.error('DOM解析时间过长:', metrics);
}
四、自动化检测方案
1. Puppeteer检测脚本
javascript
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 监听控制台错误
page.on('console', msg => {
if(msg.type() === 'error') {
console.log('页面错误:', msg.text());
}
});
// 设置超时检测
await Promise.race([
page.goto('https://your-site.com'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('页面加载超时')), 5000)
)
]);
// 检查可见内容
const content = await page.evaluate(() => {
return {
bodyText: document.body.innerText,
childCount: document.body.children.length
};
});
if(content.childCount === 0 || content.bodyText.length < 10) {
console.error('检测到白屏现象');
}
await browser.close();
})();
2. 真实用户监控(RUM)
javascript
// 使用浏览器的MutationObserver监控DOM变化
const observer = new MutationObserver((mutations) => {
if(!document.querySelector('#app')?.innerHTML) {
// 上报白屏事件
beacon.send('white-screen', {
url: location.href,
ua: navigator.userAgent
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
五、常见白屏场景示例
-
资源加载失败:
xml<!-- 错误的资源路径 --> <script src="/wrong-path/app.js"></script> -
语法错误:
javascript// 缺少括号导致整个脚本不执行 function test() { console.log('hello' } -
框架初始化失败:
arduino// Vue示例 - 挂载元素不存在 new Vue({el: '#not-exist'}); -
CSS阻塞:
xml<!-- 不正确的CSS引入阻塞渲染 --> <link rel="stylesheet" href="nonexist.css"> -
第三方库冲突:
scss// 两个库都修改了Array原型 libraryA.modifyPrototype(); libraryB.modifyPrototype(); // 冲突导致错误 ```
2.前端埋点
一、页面生命周期埋点
1. 页面加载阶段
javascript
// 记录页面开始加载时间
const pageStartTime = Date.now();
// 监听页面加载完成
window.addEventListener('load', () => {
const loadTime = Date.now() - pageStartTime;
track('page_load', {
load_time: loadTime,
referrer: document.referrer,
resource_status: checkResources()
});
});
// 检查关键资源加载状态
function checkResources() {
return performance.getEntriesByType('resource').map(res => ({
name: res.name,
type: res.initiatorType,
duration: res.duration.toFixed(2)
}));
}
2. 用户交互阶段
javascript
// 点击事件埋点(支持事件委托)
document.body.addEventListener('click', (e) => {
const target = e.target.closest('[data-track]');
if(target) {
track('element_click', {
element_id: target.id,
track_type: target.dataset.track,
position: `${e.clientX},${e.clientY}`
});
}
});
// 滚动深度记录
let maxScroll = 0;
window.addEventListener('scroll', _.throttle(() => {
const currentScroll = window.scrollY / document.body.scrollHeight;
if(currentScroll > maxScroll) {
maxScroll = currentScroll;
track('scroll_depth', { depth: Math.round(maxScroll * 100) });
}
}, 1000));
3. 页面停留时长计算
javascript
let activeStart = Date.now();
let inactiveTime = 0;
// 用户活跃状态检测
document.addEventListener('mousemove', resetActiveTimer);
document.addEventListener('keydown', resetActiveTimer);
function resetActiveTimer() {
if(inactiveTime > 0) {
track('user_inactive', { duration: inactiveTime });
inactiveTime = 0;
}
activeStart = Date.now();
}
// 每10秒检测一次活跃状态
setInterval(() => {
if(Date.now() - activeStart > 15000) { // 15秒无操作视为不活跃
inactiveTime += 10000;
} else {
track('user_active', { duration: 10000 });
}
}, 10000);
二、特殊场景处理
1. 页面隐藏/显示
javascript
// 页面可见性变化监听
document.addEventListener('visibilitychange', () => {
if(document.hidden) {
track('page_hide', {
stay_time: Date.now() - pageStartTime,
scroll_depth: maxScroll
});
} else {
track('page_show');
}
});
2. 页面关闭前上报
javascript
// 确保页面关闭前数据上报
window.addEventListener('beforeunload', () => {
const totalStay = Date.now() - pageStartTime;
navigator.sendBeacon('/api/track', JSON.stringify({
event: 'page_close',
active_time: totalStay - inactiveTime,
scroll_depth: maxScroll
}));
});
三、数据上报优化方案
1. 批量上报机制
ini
let eventQueue = [];
const MAX_QUEUE = 5;
const FLUSH_INTERVAL = 3000;
function addToQueue(event) {
eventQueue.push(event);
if(eventQueue.length >= MAX_QUEUE) {
flushQueue();
}
}
function flushQueue() {
if(eventQueue.length === 0) return;
const batchData = { batch: eventQueue };
navigator.sendBeacon('/api/batch', JSON.stringify(batchData));
eventQueue = [];
}
// 定时刷新队列
setInterval(flushQueue, FLUSH_INTERVAL);
2. 关键指标计算
php
// 计算FMP(首次有效绘制)
new PerformanceObserver((entryList) => {
const [entry] = entryList.getEntriesByName('first-contentful-paint');
track('fmp', { value: entry.startTime.toFixed(2) });
}).observe({type: 'paint', buffered: true});
// 计算LCP(最大内容绘制)
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
track('lcp', { value: lastEntry.startTime.toFixed(2) });
}).observe({type: 'largest-contentful-paint', buffered: true});
四、面试回答精简版
"我们实现全链路埋点主要分三个阶段:
-
加载阶段:
- 用
performance API采集DNS/TTFB等指标 - 监听
load事件记录完整加载时间 - 检查关键资源状态(如图片/脚本)
- 用
-
交互阶段:
- 事件委托监听全局点击(带
data-track属性) - 节流处理滚动事件计算最大深度
- 通过
mousemove/keydown检测活跃状态
- 事件委托监听全局点击(带
-
离开阶段:
visibilitychange处理页面切换beforeunload+sendBeacon确保关闭前上报- 计算总停留时长和有效活跃时间
3.那为什么大家都使用请求 GIF 图片的方式上报埋点数据呢?
-
防止跨域问题:前端监控的请求常常会遇到跨域问题,这可能会影响监控的准确性和可用性。然而,图片的src属性并不会跨域,因此使用GIF图片作为埋点可以正常发起请求,从而有效避免跨域问题。
-
防止阻塞页面加载:在创建资源节点后,通常只有当对象注入到浏览器的DOM树后,浏览器才会实际发送资源请求。但反复操作DOM会引发性能问题,且载入js/css资源会阻塞页面渲染,从而影响用户体验。与此不同,构造图片打点不需要插入DOM,只要在js中new出Image对象就能发起请求,这样就不会有阻塞问题。即使在没有js的浏览器环境中,也能通过img标签正常打点,这是其他类型的资源请求所做不到的。
-
体积小,节约流量:相比其他图片格式(如BMP和PNG),GIF图片具有更小的体积。例如,最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF只需要43个字节。因此,使用GIF作为埋点可以显著节约流量,提高数据传输效率。
-
浏览器支持性好:所有浏览器都支持Image对象,即使不支持XMLHttpRequest对象也一样。这意味着使用GIF进行埋点上报可以在各种浏览器环境中稳定运行。
-
记录错误的过程很少出错:某些情况下,如Ajax通信过程中页面跳转,请求可能会被取消。但使用图片进行埋点上报则不会遇到这个问题,特别是在记录离开页面打点行为的时候会很有用。