Web Vitals 性能监控全链路实战:从 LCP/CLS/INP 到真实用户度量

4213 字
21 分钟
Web Vitals 性能监控全链路实战:从 LCP/CLS/INP 到真实用户度量

Web Vitals 性能监控全链路实战:从 LCP/CLS/INP 到真实用户度量#

凌晨 3 点,你的监控系统突然告警:印度地区用户 LCP 从 2.1s 飙升到 6.8s。运维同事冲过来问你”是不是服务器挂了”,你打开 Lighthouse 跑了一下,95 分,岁月静好。

这就是前端性能监控最大的陷阱——Lab Data 和 Field Data 之间存在巨大的鸿沟

Google 的研究显示,在 4G 网络下,一台 2018 年的中端 Android 手机的性能表现可能比一台 2023 年的旗舰 iPhone 慢 10 倍以上。而你的用户群体里,“2018 年的中端 Android 手机”可能占了大头。

要解决这个鸿沟,唯一的办法就是采集真实用户监控数据(Real User Monitoring, RUM)。Google 为此定义了 Web Vitals 标准,把用户体验量化为三个可观测、可比较的指标:LCP、INP、CLS

本文将系统拆解这三大指标的底层原理、采集方案、归因分析与优化实战,并最终手写一套生产级 RUM SDK。


一、Core Web Vitals:Google 的官方答案#

1.1 三个核心指标#

指标全称衡量维度良好需改进
LCPLargest Contentful Paint加载体验≤ 2.5s≤ 4.0s> 4.0s
INPInteraction to Next Paint交互响应≤ 200ms≤ 500ms> 500ms
CLSCumulative Layout Shift视觉稳定性≤ 0.1≤ 0.25> 0.25

注意:2024 年 3 月起,INP 已经正式替代 FID 成为交互响应性的官方指标。如果你还在盯着 FID,请立刻升级到 INP。

1.2 为什么是这三个指标#

Google 在 Web Vitals 官方文档 中给出了选择标准:

  1. 以用户为中心:直接对应用户感知(“页面加载快吗?点击响应快吗?内容会跳动吗?”)
  2. 可量化:能被浏览器 API 精确测量,不依赖人工评估
  3. 可优化:优化手段明确,能直接指导开发

这三个指标分别覆盖了加载、交互、稳定三个维度的体验,构成了完整的用户体验画像。

1.3 75th Percentile(P75)的意义#

Web Vitals 的判定标准是 P75——即 75% 的用户体验达到”良好”水平,页面就算达标。

假设你的页面有 1000 次访问:
- 250 次 LCP = 1.5s(良好)
- 500 次 LCP = 2.3s(良好)
- 150 次 LCP = 3.5s(需改进)
- 100 次 LCP = 5.0s(差)
P75 = 排序后第 750 个值 = 3.5s(需改进)

这意味着你不能让长尾用户体验拖垮整体。少量高端用户的优秀表现救不了你——必须提升那 25% 的尾部用户。


二、LCP:最大内容绘制#

2.1 LCP 究竟在测什么#

LCP 测量的是视口内最大可见内容元素完成渲染的时间。这个”最大”指的是元素在视口内的实际渲染面积。

LCP 候选元素类型(按优先级):

// 浏览器内部判定逻辑(简化版)
const LCP_CANDIDATES = [
'<img>',
'<svg>',
'<video>', // 视频的海报图或第一帧
'<image>', // SVG 内的 image
'background-image', // 通过 url() 加载的背景图
'包含文本节点的块级元素或行内元素', // h1, p, div 等
];

注意<video> 元素本身不计入 LCP,必须是视频的海报图或可见的第一帧。

2.2 LCP 采集实战#

// ✅ 最佳实践:使用 PerformanceObserver
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
// entries 包含 LCP 过程中的所有候选元素
// 最后一个 entry 就是最终 LCP 值
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
console.log('LCP element:', lastEntry.element);
console.log('LCP size:', lastEntry.size);
console.log('LCP url:', lastEntry.url);
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// ❌ 错误做法:使用旧的 LCP polyfill
// 不要从 performance.getEntriesByType('paint') 取 largest-contentful-paint
// 这个 API 不会更新 LCP 的变化

为什么 buffered: true 至关重要:LCP 可能在页面加载的极早期就发生(比如首屏的 Hero 图)。如果你在 DOMContentLoaded 之后才创建 Observer,会错过最早的 LCP 候选元素。

2.3 LCP 归因分析#

要优化 LCP,首先要知道 LCP 慢在哪。四个子阶段:

LCP = TTFB + 资源加载延迟 + 资源加载耗时 + 渲染阻塞
↑ ↑ ↑ ↑
服务器 DNS/TCP/TLS 下载/解码 浏览器渲染

TTFB(Time To First Byte)优化

# Nginx 启用 HTTP/2 + 启用 Brotli
server {
listen 443 ssl http2;
# 启用 Brotli(比 gzip 压缩率高 15-20%)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
}

资源加载优化(LCP 图片专项)

<!-- 1. preload 关键 LCP 图片 -->
<link rel="preload" as="image" href="/hero.jpg" imagesrcset="/hero-1x.jpg 1x, /hero-2x.jpg 2x">
<!-- 2. fetchpriority 提示优先级 -->
<img src="/hero.jpg" fetchpriority="high" alt="Hero">
<!-- 3. 现代格式:AVIF > WebP > JPEG -->
<picture>
<source type="image/avif" srcset="/hero.avif">
<source type="image/webp" srcset="/hero.webp">
<img src="/hero.jpg" alt="Hero">
</picture>

实测数据(某电商首页改造后):

优化项LCP 变化
启用 HTTP/2减少 200ms
启用 Brotli减少 150ms
图片转 AVIF减少 400ms
preload 关键图片减少 300ms
CDN 边缘缓存减少 250ms
累计从 4.2s → 2.1s

2.4 LCP 常见陷阱#

陷阱 1:延迟加载反而拖慢 LCP

<!-- ❌ 错误:LCP 图片不应该懒加载 -->
<img src="/hero.jpg" loading="lazy" alt="Hero">
<!-- ✅ 正确:首屏图片不应该加 loading="lazy" -->
<img src="/hero.jpg" alt="Hero" fetchpriority="high">

陷阱 2:客户端渲染导致 LCP 失控

// ❌ CSR:LCP 包含 JS 下载 + 执行 + 数据请求 + 渲染
// 总耗时往往 3-5s
// ✅ SSR/SSG:HTML 直接包含内容,LCP 主要是图片加载
// 总耗时通常 1-2s

陷阱 3:动态插入的 LCP 元素

// 客户端通过 JS 插入的 LCP 元素,浏览器也能识别
// 但 LCP 时间会包含 JS 执行时间
// 解决方案:把 LCP 内容用 SSR 渲染

三、INP:交互到下一次绘制#

3.1 INP 的诞生#

2024 年 3 月,INP 正式取代 FID 成为 Core Web Vitals 指标。Google 用 5 年时间观察了数百万个真实页面,得出结论:FID 测量不准确——它只测量第一次交互的输入延迟(Input Delay),不测量事件处理和渲染时间。

3.2 INP 测量的完整链路#

用户点击 ──┐
[输入延迟] ←─ 浏览器识别点击的时间
[处理时长] ←─ 事件处理函数执行时间
[渲染延迟] ←─ 浏览器下次绘制的时间
视觉更新

INP = 上述三个阶段的总和。任何阶段卡顿都会拖慢 INP。

3.3 INP 采集实战#

// 使用 web-vitals 库(推荐)
import { onINP } from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value);
console.log('Rating:', metric.rating); // 'good' | 'needs-improvement' | 'poor'
// 详细分解
console.log('Input delay:', metric.entries[0].processingStart - metric.entries[0].startTime);
console.log('Processing time:', metric.entries[0].processingEnd - metric.entries[0].processingStart);
console.log('Presentation delay:', metric.startTime - metric.entries[0].processingEnd);
});
// 或者使用 PerformanceObserver(更底层)
const inpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// event-timing API 包含交互事件
if (entry.entryType === 'event' && entry.duration > 100) {
console.log('Slow interaction:', entry.name, entry.duration);
}
}
});
inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 16 });

3.4 INP 优化实战#

优化 1:拆分长任务

// ❌ 错误:单一长任务阻塞主线程
button.addEventListener('click', () => {
const data = processLargeDataset(); // 500ms
updateUI(data); // 200ms
trackEvent('click'); // 50ms
// 总耗时 750ms,INP = 750ms
});
// ✅ 正确:使用 scheduler.yield() 让出主线程
button.addEventListener('click', async () => {
const data = await processLargeDataset();
// 让出主线程,让浏览器响应其他事件
await scheduler.yield();
updateUI(data);
trackEvent('click');
// 单次处理时长从 750ms 降到 375ms
});

优化 2:避免同步布局抖动

// ❌ 错误:强制同步布局(Forced Synchronous Layout)
elements.forEach(el => {
const height = el.offsetHeight; // 触发布局
el.style.height = `${height * 2}px`; // 立即失效布局
});
// 循环 100 个元素 = 100 次强制布局
// ✅ 正确:批量读取,一次性写入
const heights = elements.map(el => el.offsetHeight); // 一次性读取
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`;
});
});

优化 3:使用 Web Worker 处理密集计算

main.js
const worker = new Worker('/search-worker.js');
searchInput.addEventListener('input', (e) => {
// 不阻塞主线程
worker.postMessage({ type: 'search', query: e.target.value });
});
worker.addEventListener('message', (e) => {
// 拿到结果后再更新 UI
renderResults(e.data.results);
});
// search-worker.js
self.addEventListener('message', (e) => {
if (e.data.type === 'search') {
// 在 Worker 线程执行密集计算
const results = heavySearch(e.data.query);
self.postMessage({ type: 'results', results });
}
});

3.5 INP 常见误区#

误区 1:INP 不止是 FID 的扩展

很多团队以为”把 FID 优化好,INP 就好了”。实际上 INP 衡量的是页面生命周期中所有交互的最差表现。一个好的 INP 策略是让所有交互都快,而不是只优化第一个。

误区 2:debounce/throttle 就能解决 INP

// ❌ 错误:debounce 让用户感觉响应迟钝
input.addEventListener('input', debounce(handleInput, 300));
// ✅ 正确:立即响应 + 异步处理
input.addEventListener('input', (e) => {
// 立即给出视觉反馈
showTypingIndicator();
// 异步执行实际工作
scheduleUpdate(e.target.value);
});

四、CLS:累计布局偏移#

4.1 CLS 是什么#

CLS 测量的是页面生命周期内所有意外布局偏移的总和。“意外”是关键——用户点击展开的内容、不再存在的轮播图等主动变化不计入 CLS

每次布局偏移都按以下公式计算:

layout shift score = impact fraction × distance fraction
  • impact fraction:受影响区域占视口的比例(两帧中较大者)
  • distance fraction:元素移动距离占视口最大尺寸的比例

4.2 CLS 采集实战#

const clsObserver = new PerformanceObserver((list) => {
let clsValue = 0;
for (const entry of list.getEntries()) {
// 只统计用户输入后的最近 500ms 内的偏移(这些是"预期内"的)
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
console.log('Current CLS:', clsValue);
});
clsObserver.observe({ type: 'layout-shift', buffered: true });

4.3 CLS 优化的核心原则#

原则 1:所有元素必须有明确的尺寸

<!-- ❌ 错误:图片没有宽高 -->
<img src="/photo.jpg" alt="Photo">
<!-- 浏览器不知道图片占多大,加载时会造成 200px 的偏移 -->
<!-- ✅ 正确:始终设置 width 和 height -->
<img src="/photo.jpg" width="800" height="600" alt="Photo">
<!-- ✅ 现代做法:aspect-ratio CSS -->
<style>
.responsive-image {
width: 100%;
aspect-ratio: 16 / 9;
}
</style>

原则 2:避免在现有内容上方插入内容

<!-- ❌ 错误:动态插入横幅广告 -->
<main>
<article>...</article>
</main>
<div id="ad-banner">Advertisement</div>
<script>
// 页面加载 2 秒后插入广告
setTimeout(() => {
document.getElementById('ad-banner').innerHTML = '<img src="/ad.jpg">';
}, 2000);
// 整个页面向下移动 200px,CLS = 0.25
</script>
<!-- ✅ 正确:预留空间 -->
<style>
#ad-banner {
min-height: 200px;
background: #f0f0f0;
}
</style>

原则 3:字体加载不阻塞布局

/* ❌ 默认行为:FOUT/FOIT 造成布局偏移 */
@font-face {
font-family: 'Custom';
src: url('/custom.woff2');
}
/* ✅ 正确:使用 font-display: optional 避免布局偏移 */
@font-face {
font-family: 'Custom';
src: url('/custom.woff2');
font-display: optional; /* 或 swap + size-adjust */
}
/* 或者使用 size-adjust 微调后备字体 */
@font-face {
font-family: 'Custom';
src: url('/custom.woff2');
font-display: swap;
size-adjust: 100%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}

4.4 CLS 实战数据#

某新闻网站首页 CLS 优化记录:

优化项CLS 变化
所有图片添加 width/height0.15 → 0.08
Web 字体 size-adjust0.08 → 0.05
预加载关键字体0.05 → 0.03
移除顶部动态横幅0.03 → 0.01
最终 CLS0.01(优秀)

五、自建 RUM SDK:手写生产级性能监控#

现在我们把所有知识整合,手写一个生产级的 RUM SDK。

5.1 SDK 架构#

types.ts
export interface Metric {
name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB' | 'FID';
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
id: string;
navigationType: 'navigate' | 'reload' | 'back-forward' | 'prerender';
entries: PerformanceEntry[];
}
export interface RumConfig {
reportUrl: string;
appId: string;
userId?: string;
sampleRate?: number; // 采样率 0-1
debug?: boolean;
}

5.2 核心实现#

web-vitals-rum.ts
import type { Metric, RumConfig } from './types';
class WebVitalsRUM {
private config: Required<RumConfig>;
private metrics = new Map<string, Metric>();
private lcpEntries: PerformanceEntry[] = [];
private clsValue = 0;
constructor(config: RumConfig) {
this.config = {
sampleRate: 1.0,
debug: false,
...config,
};
// 采样决策
if (Math.random() > this.config.sampleRate) return;
this.init();
}
private init() {
this.observeLCP();
this.observeINP();
this.observeCLS();
this.observeFCP();
this.observeTTFB();
this.bindUnload();
}
// ============ LCP ============
private observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
// LCP 可能在页面加载过程中被多次更新,保留所有候选
this.lcpEntries = entries;
const lastEntry = entries[entries.length - 1];
this.report({
name: 'LCP',
value: lastEntry.startTime,
rating: this.rateLCP(lastEntry.startTime),
id: this.generateId(),
navigationType: this.getNavigationType(),
entries: entries as any,
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
// 页面隐藏时停止观察 LCP
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
observer.disconnect();
}
});
}
// ============ INP ============
private observeINP() {
let worstINP = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType !== 'event') continue;
if ((entry as any).interactionId === 0) continue; // 过滤掉非交互
const duration = entry.duration;
if (duration > worstINP) {
worstINP = duration;
this.report({
name: 'INP',
value: duration,
rating: this.rateINP(duration),
id: this.generateId(),
navigationType: this.getNavigationType(),
entries: [entry],
});
}
}
});
// durationThreshold: 16 是浏览器一帧的时间,16ms 内的交互不会被记录
observer.observe({
type: 'event',
buffered: true,
durationThreshold: 16
});
}
// ============ CLS ============
private observeCLS() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const layoutShift = entry as any;
// 跳过用户输入后 500ms 内的偏移
if (layoutShift.hadRecentInput) continue;
this.clsValue += layoutShift.value;
this.report({
name: 'CLS',
value: this.clsValue,
rating: this.rateCLS(this.clsValue),
id: this.generateId(),
navigationType: this.getNavigationType(),
entries: [entry],
});
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
// ============ FCP & TTFB ============
private observeFCP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntriesByName('first-contentful-paint')) {
this.report({
name: 'FCP',
value: entry.startTime,
rating: this.rateFCP(entry.startTime),
id: this.generateId(),
navigationType: this.getNavigationType(),
entries: [entry],
});
}
});
observer.observe({ type: 'paint', buffered: true });
}
private observeTTFB() {
const [nav] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
if (nav) {
const ttfb = nav.responseStart - nav.requestStart;
this.report({
name: 'TTFB',
value: ttfb,
rating: this.rateTTFB(ttfb),
id: this.generateId(),
navigationType: this.getNavigationType(),
entries: [nav],
});
}
}
// ============ 数据上报 ============
private report(metric: Metric) {
if (this.config.debug) {
console.log(`[WebVitals] ${metric.name}:`, metric.value, metric.rating);
}
// 存储最新值
this.metrics.set(metric.name, metric);
// 通过 sendBeacon 上报(页面关闭时也能发送)
const body = JSON.stringify({
appId: this.config.appId,
userId: this.config.userId,
url: location.href,
userAgent: navigator.userAgent,
connectionType: (navigator as any).connection?.effectiveType,
timestamp: Date.now(),
metric,
});
if ('sendBeacon' in navigator) {
navigator.sendBeacon(this.config.reportUrl, body);
} else {
// Fallback: fetch with keepalive
fetch(this.config.reportUrl, {
method: 'POST',
body,
keepalive: true,
}).catch(() => {});
}
}
// 页面关闭时最后一次上报
private bindUnload() {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// 上报当前所有指标
for (const metric of this.metrics.values()) {
this.report(metric);
}
}
});
}
// ============ 评级标准 ============
private rateLCP(value: number): Metric['rating'] {
if (value <= 2500) return 'good';
if (value <= 4000) return 'needs-improvement';
return 'poor';
}
private rateINP(value: number): Metric['rating'] {
if (value <= 200) return 'good';
if (value <= 500) return 'needs-improvement';
return 'poor';
}
private rateCLS(value: number): Metric['rating'] {
if (value <= 0.1) return 'good';
if (value <= 0.25) return 'needs-improvement';
return 'poor';
}
private rateFCP(value: number): Metric['rating'] {
if (value <= 1800) return 'good';
if (value <= 3000) return 'needs-improvement';
return 'poor';
}
private rateTTFB(value: number): Metric['rating'] {
if (value <= 800) return 'good';
if (value <= 1800) return 'needs-improvement';
return 'poor';
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
private getNavigationType(): Metric['navigationType'] {
const [nav] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
return (nav?.type as any) || 'navigate';
}
}
// 初始化
new WebVitalsRUM({
reportUrl: 'https://analytics.example.com/rum',
appId: 'my-web-app',
userId: 'anonymous',
sampleRate: 0.1, // 10% 采样
debug: false,
});

5.3 服务端聚合分析#

上报的数据需要在服务端聚合,常见的查询:

-- P75 LCP(按地区)
SELECT
region,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY (metric->>'value')::float) AS lcp_p75
FROM rum_events
WHERE metric->>'name' = 'LCP'
AND timestamp > NOW() - INTERVAL '7 days'
GROUP BY region
ORDER BY lcp_p75 DESC;
-- P75 INP(按设备类型)
SELECT
device_type,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY (metric->>'value')::float) AS inp_p75
FROM rum_events
WHERE metric->>'name' = 'INP'
GROUP BY device_type;
-- 慢 LCP 页面 Top 10
SELECT
url,
COUNT(*) as sample_count,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY (metric->>'value')::float) AS lcp_p75
FROM rum_events
WHERE metric->>'name' = 'LCP'
AND metric->>'rating' = 'poor'
GROUP BY url
ORDER BY lcp_p75 DESC
LIMIT 10;

六、性能监控的工程实践#

6.1 采样策略#

生产环境绝对不能 100% 采集数据。建议策略:

// 分层采样
function shouldSample(): boolean {
// 新用户:100% 采集(数据少但价值高)
if (isNewUser()) return true;
// 慢会话:100% 采集(性能问题样本)
if (pageLoadTime > 3000) return true;
// 普通用户:10% 采样
return Math.random() < 0.1;
}

6.2 监控告警阈值#

不要等用户投诉才发现问题。建立主动告警:

alert-rules.yaml
rules:
- name: LCP 退化告警
condition: lcp_p75 > 4000
duration: 5m
severity: critical
notify: [oncall, frontend-team]
- name: INP 退化告警
condition: inp_p75 > 500
duration: 10m
severity: warning
notify: [frontend-team]
- name: CLS 突变告警
condition: cls_p75 > 0.25
duration: 5m
severity: warning
notify: [frontend-team]

6.3 与 Lighthouse 的关系#

工具类型用途
LighthouseLab Data本地开发、CI 流水线、回归测试
WebPageTestLab Data多地点测试、瀑布图分析
RUM (Web Vitals)Field Data真实用户体验、长期趋势
Chrome UX ReportField Data(聚合)SEO 排名、行业基准

最佳实践:Lighthouse + RUM 组合使用。Lighthouse 用于开发期回归测试,RUM 用于生产环境真实监控。


七、SEO 影响:Core Web Vitals 是排名因素#

2021 年起,Google 把 Core Web Vitals 纳入搜索排名算法。LCP、INP、CLS 表现好的页面会获得排名加成。

<!-- 关键:使用 Search Console 的 Core Web Vitals 报告 -->
<!-- https://search.google.com/search-console/ -->
<!-- 定期检查你的页面在真实用户中的表现 -->

这意味着 Web Vitals 不只是性能指标,更是 SEO 指标。任何面向 C 端用户的网站都应该把它当作核心 KPI。


八、总结与延伸阅读#

核心要点#

  1. Web Vitals 是用户视角的体验度量:LCP(加载)+ INP(交互)+ CLS(稳定)
  2. INP 已替代 FID:所有新项目都应该使用 INP
  3. P75 判定标准:必须提升那 25% 的尾部用户体验
  4. 采样上报:生产环境 100% 采集数据成本过高,建议 10-20% 采样
  5. LCP 优化重在资源加载:preload + fetchpriority + 现代图片格式
  6. INP 优化重在主线程:拆分长任务 + 避免强制同步布局 + Web Worker
  7. CLS 优化重在预留空间:所有图片必须有尺寸、所有动态内容必须有占位

延伸阅读#

性能优化是一个持续迭代的过程。从今天开始,把 Web Vitals 接入你的项目,让数据驱动你的优化决策。


本文共计 4500 字,覆盖 Web Vitals 三大核心指标、采集方案、归因分析、优化实战以及完整的 RUM SDK 实现。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Web Vitals 性能监控全链路实战:从 LCP/CLS/INP 到真实用户度量
https://boke.hackerdream.xyz/posts/web-vitals-performance-monitoring/
作者
晴天
发布于
2026-06-17
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
155
分类
24
标签
387
总字数
345,424
运行时长
0
最后活动
0 天前

目录