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

16,344 阅读5分钟

😅【现场还原】

"前端为什么没有自己的数据库?把数据存前端不就解决了后端性能问题" ——当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线程池,使得复杂筛选的耗时从秒级降至毫秒级——但这已经是另一个技术突围的故事了。