13 KiB
13 KiB
DNS 查询日志加载性能优化方案
问题分析
当前日志详情页面加载时间约 10 秒,主要性能瓶颈分析如下:
1. 主要性能瓶颈
1.1 同步 API 调用(最严重)
- 位置:
updateLogsTable()函数第 1221 行 - 问题: 对每条日志都调用
await isDomainInTrackerDatabase(log.domain) - 影响: 假设每页 10 条日志,每条日志检查跟踪器数据库需要 100-500ms,总计 1-5 秒
- 代码:
const trackerInfo = await isDomainInTrackerDatabase(log.domain); // ❌ 每条都 await
1.2 跟踪器数据库加载
- 位置:
isDomainInTrackerDatabase()函数 - 问题: 每次检查都要等待数据库加载完成
- 影响: 首次加载或数据库未加载完成时,每条日志都要等待
1.3 域名信息 API 调用
- 位置:
getDomainInfoFromAPI()函数 - 问题: 如果实现中调用 API,会产生网络延迟
- 影响: 每个域名都可能产生一次 HTTP 请求
1.4 IP 地理位置查询
- 位置:
getIpGeolocation()函数 - 问题: 虽然有缓存,但首次查询仍需调用外部 API
- 影响: 每个新 IP 地址需要 200-500ms
2. 性能测试数据(估算)
| 操作 | 单次耗时 | 每页 10 条总耗时 | 优化后耗时 |
|---|---|---|---|
| 跟踪器检查 | 100-500ms | 1-5 秒 | <100ms |
| IP 地理位置 | 200-500ms | 2-5 秒 | <500ms(缓存) |
| 后端 API 查询 | 100-300ms | 100-300ms | 不变 |
| DOM 渲染 | 50-100ms | 50-100ms | 不变 |
| 总计 | 10 秒 | 3-10 秒 | <1 秒 |
优化方案
方案一:异步并行处理(推荐优先实施)⭐⭐⭐⭐⭐
1.1 批量并行检查跟踪器
问题: 当前是串行等待每个域名的检查结果
解决: 使用 Promise.all() 并行处理所有域名的检查
// 优化前(串行)
for (const log of logs) {
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
// ... 处理逻辑
}
// 优化后(并行)
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
const trackerResults = await Promise.all(trackerChecks);
// 然后渲染表格
logs.forEach((log, index) => {
const trackerInfo = trackerResults[index];
// ... 渲染逻辑
});
预期效果: 从 1-5 秒降低到 100-500ms
1.2 预加载跟踪器数据库
问题: 每次检查都要等待数据库加载 解决: 页面初始化时立即加载,而不是延迟加载
// 在页面加载时立即执行
document.addEventListener('DOMContentLoaded', () => {
loadTrackersDatabase(); // 提前加载,不等待日志显示
});
预期效果: 消除数据库加载等待时间
方案二:缓存优化 ⭐⭐⭐⭐
2.1 增强跟踪器检查缓存
问题: 相同的域名被重复检查 解决: 添加内存缓存,避免重复检查
// 添加缓存
const trackerCache = new Map();
const trackerCacheTimestamp = new Map();
const TRACKER_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 小时
async function isDomainInTrackerDatabase(domain) {
// 检查缓存
const cached = trackerCache.get(domain);
const timestamp = trackerCacheTimestamp.get(domain);
if (cached && timestamp && (Date.now() - timestamp) < TRACKER_CACHE_EXPIRY) {
return cached;
}
// 执行实际检查...
const result = // ... 检查逻辑
// 缓存结果
trackerCache.set(domain, result);
trackerCacheTimestamp.set(domain, Date.now());
return result;
}
预期效果: 重复域名检查从 100ms 降低到 <1ms
2.2 IP 地理位置缓存优化
问题: 相同 IP 被重复查询 解决: 已有 LRU 缓存,确保缓存命中率高
// 确保缓存大小足够(当前为 1000,建议保持)
const GEOLOCATION_CACHE_MAX_SIZE = 1000;
方案三:延迟渲染优化 ⭐⭐⭐
3.1 先渲染后更新
问题: 等待所有数据获取完成才渲染表格 解决: 先渲染基本内容,再异步更新增强信息
async function updateLogsTable(logs) {
// 第一步:立即渲染基本表格(不等待跟踪器检查)
renderBasicTable(logs);
// 第二步:异步获取跟踪器信息并更新
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
Promise.all(trackerChecks).then(results => {
updateTrackerIcons(results); // 只更新图标部分
});
// 第三步:异步获取 IP 地理位置
logs.forEach(log => {
getIpGeolocation(log.clientIP).then(location => {
updateLocationElement(log.clientIP, location);
});
});
}
预期效果: 用户感知加载时间从 10 秒降低到 500ms 以内
3.2 虚拟滚动(针对大量数据)
问题: 如果用户选择每页 100 条,DOM 节点过多 解决: 只渲染可见区域的日志
// 使用虚拟滚动库或自定义实现
// 只渲染可视区域的 20-30 条,滚动时动态加载
预期效果: 渲染时间从 100ms 降低到 20ms
方案四:后端优化 ⭐⭐⭐⭐
4.1 后端批量检查跟踪器
问题: 前端检查每条日志的跟踪器状态 解决: 后端在返回日志时直接包含跟踪器信息
// 后端代码修改
type QueryLog struct {
// ... 现有字段
IsTracker bool `json:"isTracker"`
TrackerName string `json:"trackerName,omitempty"`
}
// 在返回日志前批量检查
func (s *Server) enrichLogsWithTrackerInfo(logs []QueryLog) {
for i := range logs {
logs[i].IsTracker, logs[i].TrackerName = s.checkTracker(logs[i].Domain)
}
}
预期效果: 完全消除前端跟踪器检查时间
4.2 后端缓存优化
问题: 后端查询数据库慢 解决: 增加 Redis 或内存缓存
// 使用内存缓存查询结果
var queryCache = lru.New(100)
预期效果: 后端查询时间从 300ms 降低到 50ms
方案五:数据库索引优化 ⭐⭐⭐
5.1 添加数据库索引
问题: 日志查询慢 解决: 为常用查询字段添加索引
-- 为时间戳添加索引(加速时间范围查询)
CREATE INDEX idx_timestamp ON query_logs(timestamp DESC);
-- 为域名添加索引(加速域名搜索)
CREATE INDEX idx_domain ON query_logs(domain);
-- 为客户端 IP 添加索引
CREATE INDEX idx_client_ip ON query_logs(client_ip);
-- 组合索引(加速组合查询)
CREATE INDEX idx_timestamp_result ON query_logs(timestamp DESC, result);
预期效果: 后端查询时间从 500ms 降低到 50ms
实施优先级
第一阶段(立即实施,效果最明显)
- ✅ 方案 1.1: 并行处理跟踪器检查
- ✅ 方案 2.1: 添加跟踪器检查缓存
- ✅ 方案 1.2: 预加载跟踪器数据库
预期效果: 10 秒 → 2-3 秒
第二阶段(本周内实施)
- ✅ 方案 3.1: 先渲染后更新
- ✅ 方案 4.1: 后端批量检查跟踪器
预期效果: 2-3 秒 → 1 秒以内
第三阶段(长期优化)
- ✅ 方案 5.1: 数据库索引优化
- ✅ 方案 4.2: 后端缓存
- ✅ 方案 3.2: 虚拟滚动
预期效果: 1 秒 → 500ms 以内
具体代码修改
修改 1: logs.js - 并行处理跟踪器检查
// 在 updateLogsTable() 函数中,替换第 1155-1353 行
async function updateLogsTable(logs) {
const tableBody = document.getElementById('logs-table-body');
if (!tableBody) return;
// 清空表格
tableBody.innerHTML = '';
if (logs.length === 0) {
// 显示空状态
tableBody.innerHTML = `
<tr>
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
</tr>
`;
return;
}
// 检测是否为移动设备
const isMobile = window.innerWidth <= 768;
// 【优化】并行处理所有跟踪器检查
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
const trackerResults = await Promise.all(trackerChecks);
// 预创建文档片段,减少 DOM 操作
const fragment = document.createDocumentFragment();
// 填充表格
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
const trackerInfo = trackerResults[i];
const isTracker = trackerInfo !== null;
const row = document.createElement('tr');
row.className = `border-b border-gray-100 hover:bg-gray-50 transition-colors ${
log.result === 'blocked' ? 'bg-red-50' : ''
}`;
// ... 渲染逻辑(保持不变,但移除 await)
fragment.appendChild(row);
}
// 一次性添加到 DOM
tableBody.appendChild(fragment);
// 【优化】异步更新 IP 地理位置(不阻塞渲染)
logs.forEach(log => {
if (!isPrivateIP(log.clientIP)) {
getIpGeolocation(log.clientIP).then(location => {
const locationEl = document.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
if (locationEl) {
locationEl.textContent = location;
}
});
}
});
}
修改 2: logs.js - 添加跟踪器缓存
// 在全局变量部分添加
const trackerCache = new Map();
const trackerCacheTimestamp = new Map();
const TRACKER_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 小时
// 修改 isDomainInTrackerDatabase 函数
async function isDomainInTrackerDatabase(domain) {
// 检查缓存
const cached = trackerCache.get(domain);
const timestamp = trackerCacheTimestamp.get(domain);
if (cached !== undefined && timestamp && (Date.now() - timestamp) < TRACKER_CACHE_EXPIRY) {
return cached;
}
// 确保数据库已加载
if (!trackersDatabase || !trackersLoaded) {
await loadTrackersDatabase();
}
if (!trackersDatabase || !trackersDatabase.trackers) {
trackerCache.set(domain, null);
trackerCacheTimestamp.set(domain, Date.now());
return null;
}
// 检查逻辑(保持不变)
let result = null;
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
result = trackersDatabase.trackers[domain];
} else {
for (const trackerKey in trackersDatabase.trackers) {
const tracker = trackersDatabase.trackers[trackerKey];
if (tracker && tracker.url) {
try {
const trackerUrl = new URL(tracker.url);
if (trackerUrl.hostname === domain) {
result = tracker;
break;
}
} catch (e) {
// 忽略无效 URL
}
}
}
}
// 缓存结果
trackerCache.set(domain, result);
trackerCacheTimestamp.set(domain, Date.now());
// 限制缓存大小(LRU 简单实现)
if (trackerCache.size > 1000) {
const oldestKey = trackerCache.keys().next().value;
trackerCache.delete(oldestKey);
trackerCacheTimestamp.delete(oldestKey);
}
return result;
}
修改 3: index.html - 预加载跟踪器数据库
<!-- 在页面底部,logs.js 加载后立即执行 -->
<script>
// 页面加载时立即预加载跟踪器数据库
document.addEventListener('DOMContentLoaded', function() {
// 异步加载,不阻塞页面
if (typeof loadTrackersDatabase === 'function') {
loadTrackersDatabase();
}
});
</script>
性能监控
添加性能日志
// 在 loadLogs() 函数中添加性能监控
async function loadLogs() {
const startTime = performance.now();
console.log('🚀 开始加载日志...');
// ... 加载逻辑
const endTime = performance.now();
const duration = endTime - startTime;
console.log(`✅ 日志加载完成,耗时:${duration.toFixed(2)}ms`);
// 分解各阶段耗时
console.log('📊 性能分析:', {
API 请求:apiTime,
跟踪器检查:trackerTime,
DOM 渲染:renderTime,
IP 查询:ipTime
});
}
预期最终效果
| 优化阶段 | 加载时间 | 改善幅度 |
|---|---|---|
| 优化前 | 10 秒 | - |
| 第一阶段 | 2-3 秒 | 70-80% |
| 第二阶段 | 1 秒以内 | 90% |
| 第三阶段 | 500ms 以内 | 95% |
风险与注意事项
- 内存使用: 缓存会增加内存占用,建议限制缓存大小
- 缓存一致性: 跟踪器数据库更新后需要清理缓存
- 浏览器兼容性:
Promise.all()在老旧浏览器可能不支持 - 并发限制: 如果日志数量过多(>100),考虑分批处理
总结
最优先实施的优化:
- ✅ 并行处理跟踪器检查(方案 1.1)
- ✅ 添加跟踪器检查缓存(方案 2.1)
- ✅ 先渲染后更新(方案 3.1)
这三项优化可以将加载时间从 10 秒降低到 1 秒以内,且实施成本低,风险小。