注册
web

后端出身的CTO问:"前端为什么没有数据库?",我直接无语......

😅【现场还原】


"前端为什么没有自己的数据库?把数据存前端不就解决了后端性能问题" ——当CTO抛出这个灵魂拷问时,会议室突然安静得能听见CPU风扇的嗡鸣,在座所有人都无语了。这场因后端性能瓶颈引发的技术博弈,最终以"前端分页查询+本地筛选"的妥协方案告终。


面对现在几乎所有公司的技术leader都是后端出身,有的不懂前端甚至不懂技术,作为前端开发者,我们真的只能被动接受吗?


😣【事情背景】



  • 需求:前端展示所有文章的标签列表,用户可以选择标签筛选文章,支持多选,每个文章可能有多个标签,也可能没任何标签。
  • 前端观点:针对这种需求,我自然想到用户选中标签后,将标签id传给后端,后端根据id筛选文章列表返回即可。
  • 后端观点:后端数据分库分表,根据标签检索数据还要排序分页,有性能瓶颈会很慢,很慢就会导致天天告警。
  • 上升决策:由于方案有上述分歧,我们就找来了双方leader决策,双方leader也有分歧,最终叫来了CTO。领导想让我们将数据定时备份到前端,需要筛选的时候前端自己筛选。

    CTO语录


    “前端为什么没有数据库?,把数据存前端,前端筛选,数据库不就没有性能压力了”


    "现在手机性能比服务器还强,让前端存全量数据怎么了?"


    "IndexedDB不是数据库?localStorage不能存JSON?"


    "分页?让前端自己遍历数组啊,这不就是你们说的'前端工程化'吗?"





😓【折中方案】


在方案评审会上,我们据理力争:



  1. 分页请求放大效应:用户等待时间=单次请求延迟×页数
  2. 内存占用风险:1万条数据在移动端直接OOM
  3. 数据一致性难题:轮询期间数据更新的同步问题

但现实往往比代码更复杂——当CTO拍板要求"先实现再优化",使用了奇葩的折中方案:



  • 前端轮询获取前1000条数据做本地筛选,用户分页获取数据超过1000条后,前端再轮询获取1000条,以此类推。
  • 前端每页最多获取50条数据,每次最多并发5个请求(后端要求)

只要技术监控不报错,至于用户体验?慢慢等着吧你......


🖨️【批量并发请求】


既然每页只有50条数据,那我至少得发20个请求来拿到所有数据。显然,逐个请求会让用户等待很长时间,明显不符合前端性能优化的原则。于是我选择了 p-limitPromise.all来实现异步并发线程池。通过并发发送多个请求,可以大大减少数据获取的总时间。


import pLimit from 'p-limit';
const limit = pLimit(5); // 限制最多5个并发请求

// 模拟接口请求
const fetchData = (page, pageSize) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`数据页 ${page}${pageSize}条数据`);
}, 1000);
});
};

// 异步任务池
const runTasks = async () => {
const totalData = 1000; // 总数据量
const pageSize = 50; // 每页容量

const totalPages = Math.ceil(totalData / pageSize); // 计算需要多少页

const tasks = [];

// 根据总页数动态创建请求任务
for (let i = 1; i <= totalPages; i++) {
tasks.push(limit(() => fetchData(i, pageSize))); // 使用pLimit限制并发请求
}

const results = await Promise.all(tasks); // 等待所有请求完成
console.log('已完成所有任务:', results);
};

runTasks();



📑【高效本地筛选数据】


当所有数据都请求回来了,下一步就是进行本地筛选。毕竟后端已经将查询任务分配给了前端,所以我得尽可能让筛选的过程更高效,避免在本地做大量的计算导致性能问题。


1. 使用哈希进行高效查找


如果需要根据某个标签来筛选数据,最直接的做法就是遍历整个数据集,但这显然效率不高。于是我决定使用哈希表(或 Map)来组织数据。这样可以在常数时间内完成筛选操作。


const filterDataByTag = (data, tag) => {
const tagMap = new Map();

data.forEach(item => {
if (!tagMap.has(item.tag)) {
tagMap.set(item.tag, []);
}
tagMap.get(item.tag).push(item);
});

return tagMap.get(tag) || [];
};

const result = filterDataByTag(allData, 'someTag');
console.log(result);


2. 使用 Web Workers 进行数据处理


如果数据量很大,筛选过程可能会比较耗时,导致页面卡顿。为了避免这个问题,可以将数据筛选的过程交给 Web Workers 处理。Web Worker 可以在后台线程运行,避免阻塞主线程,从而让用户体验更加流畅。


const worker = new Worker('worker.js');

worker.postMessage(allData);

worker.onmessage = function(event) {
const filteredData = event.data;
console.log('筛选后的数据:', filteredData);
};

// worker.js
onmessage = function(e) {
const data = e.data;
const filteredData = data.filter(item => item.tag === 'someTag');
postMessage(filteredData);
};


📝【总结】


这场技术博弈给我们带来三点深刻启示:



  1. 数据民主化趋势:随着WebAssembly、WebGPU等技术的发展,前端正在获得堪比后端的计算能力
  2. 妥协的艺术:临时方案必须包含演进路径,我们的分页实现预留了切换GraphQL的接口
  3. 性能新思维:从前端到边缘计算,性能优化正在从"减少请求"转向"智能分发"

站在CTO那句"前端为什么没有数据库"的肩膀上,我们正在构建这样的未来:每个前端应用都内置轻量级数据库内核,通过差异同步策略与后端保持数据一致,利用浏览器计算资源实现真正的端智能。这不是妥协的终点,而是下一代Web应用革命的起点。


后记:三个月后,我们基于SQL.js实现了前端SQL查询引擎,配合WebWorker线程池,使得复杂筛选的耗时从秒级降至毫秒级——但这已经是另一个技术突围的故事了。


作者:VeryCool
来源:juejin.cn/post/7472732247932174388

0 个评论

要回复文章请先登录注册