在前端性能优化工作中,Resource Timing API 是浏览器原生提供的资源加载度量工具。它能够精确记录每一个图片、脚本、样式表、接口请求从开始到结束各阶段的时间戳。然而这些原始数据仅存在于当前页面会话中,若想跨页面、跨用户做长期分析与问题排查,就需要一个轻量且易嵌入的存储方案,SQLite 正是此类场景下的理想选择。

一、Resource Timing 基础与数据结构
Resource Timing 分为两个主要类型:navigation 和 resource。navigation 描述页面本身加载过程,resource 描述具体子资源请求。通过 performance.getEntriesByType('resource') 可以拿到当前页面所有子资源条目,每个条目包含 name、startTime、responseEnd、domainLookupStart 等字段。
这些字段以高精度时间戳呈现,单位多为毫秒。由于浏览器限制,部分跨域资源会出于安全考虑隐藏详细阶段数据,此时需要使用 Timing-Allow-Origin 响应头开放权限。理解字段含义是后续写入 SQLite 的前提,例如 domainLookupEnd - domainLookupStart 即为 DNS 查询耗时。
1.1 核心字段说明
常用的关键字段包括 name(资源 URL)、initiatorType(资源类型如 script、img)、transferSize(传输字节数)、duration(总耗时)。在采集时建议同时记录 entryType 以区分 navigation 与 resource。
为了避免重复存储,可对 name 做截断或提取域名。因为完整 URL 可能带有动态参数,直接做主键会导致大量冗余。一般实践中会将域名单独提取为一列,便于后续分组统计。
二、SQLite 表结构设计
针对 Resource Timing 数据特点,建立一张资源性能表。使用自增主键、资源 URL、域名、类型、各阶段耗时及采集时间等字段。SQLite 支持无类型或弱类型列,但显式声明类型有助于工具识别。
建表时应为常用查询列如域名、类型、采集时间建立索引。万级数据下,没有索引的 LIKE 查询会明显变慢。以下为推荐表结构:
CREATE TABLE resource_timing ( id INTEGER PRIMARY KEY AUTOINCREMENT, res_name TEXT NOT NULL, domain TEXT, initiator_type TEXT, entry_type TEXT, dns_time REAL, tcp_time REAL, response_time REAL, duration REAL, transfer_size INTEGER, collect_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_domain ON resource_timing(domain); CREATE INDEX idx_collect ON resource_timing(collect_time);
2.1 字段计算方式
DNS 耗时等于 domainLookupEnd - domainLookupStart,TCP 耗时等于 connectEnd - connectStart,响应耗时可用 responseEnd - responseStart 表示。在插入前于 JavaScript 端计算好数值,能减轻数据库运算压力。
若某些字段在无权限时为 0,应在写入时做空值或负值标记,防止统计均值时被误算。可通过判断 domainLookupStart 是否为 0 来识别是否拿到完整数据。
三、前端采集与批量写入
在页面 load 事件之后,调用 performance.getEntriesByType('resource') 遍历结果。为降低写入频率,可先收集到数组,再使用 SQLite 的事务批量插入。若在 Electron 或移动端混合应用中,可直接通过本地 SQLite 库执行。
下面示例展示如何用 JavaScript 组织数据,并拼装 SQL 插入语句。注意代码内所有小于号与大于号均已转义,以符合存储规范。
function collectAndInsert(db) {
var entries = performance.getEntriesByType('resource');
var sqlParts = [];
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
var url = e.name;
var domain = url.split('/')[2] || 'unknown';
var dns = e.domainLookupEnd - e.domainLookupStart;
var tcp = e.connectEnd - e.connectStart;
var resp = e.responseEnd - e.responseStart;
sqlParts.push(
"('" + url + "','" + domain + "','" +
e.initiatorType + "','resource'," +
dns + "," + tcp + "," + resp + "," +
e.duration + "," + e.transferSize + ")"
);
}
if (sqlParts.length === 0) return;
var sql = "INSERT INTO resource_timing (res_name,domain,initiator_type,entry_type,dns_time,tcp_time,response_time,duration,transfer_size) VALUES " + sqlParts.join(',');
db.run(sql);
}
3.1 事务提升性能
单条 INSERT 在百条数据时就会有肉眼可见延迟。使用 BEGIN TRANSACTION 与 COMMIT 包裹批量插入,能将写入耗时从秒级降至毫秒级。在 Node.js 的 sqlite3 模块中,可使用 db.serialize 配合 db.run('BEGIN') 实现。
另外建议限制单次采集条目数,例如只保留耗时超过 100 毫秒的资源,过滤掉大量极小图标,既节省空间又突出慢请求。
四、用 SQL 做资源分析
数据落盘后,即可发挥 SQLite 的查询优势。比如统计各域名平均 DNS 与响应时间,快速定位劣质 CDN。以下查询按域名分组:
SELECT domain, COUNT(*) AS cnt, AVG(dns_time) AS avg_dns, AVG(tcp_time) AS avg_tcp, AVG(response_time) AS avg_resp FROM resource_timing WHERE entry_type = 'resource' GROUP BY domain ORDER BY avg_resp DESC LIMIT 20;
4.1 对比内存数组方案
若仅用前端内存数组存储,万条记录排序与过滤需手写循环,且无法持久化。SQLite 借助索引与查询优化器,在相同数据量下分组统计通常快数倍,并允许离线用命令行或可视化工具复查。
常见误区是把整批 Timing 数据直接 JSON.stringify 存为一个字段。这种做法虽简单,却丧失按阶段筛选能力,后期想查某个域名必须解析全部文本,效率极低。
五、注意事项与扩展
Resource Timing 缓冲区有上限,超过后早期条目会被丢弃。可监听 onresourcetimingbufferfull 事件并调用 performance.clearResourceTimings() 前先采集。对于 SPA 应用,路由切换时的资源也应周期性收集。
SQLite 文件可随应用打包或放置用户目录,配合定时任务上传至服务端做横向对比。若需更细粒度,可将 navigation 条目也写入同表,通过 entry_type 区分,统一分析首屏与子资源性能。
SQLiteResource_Timing前端性能监控修改时间:2026-08-12 01:18:31