localStorage 避坑指南:序列化、按 id 删除与 CRUD 封装

发布时间:2026/10/1 9:26:42

localStorage 避坑指南:序列化、按 id 删除与 CRUD 封装 localStorage 这个东西说简单是真简单一个setItem、一个getItem两行 JavaScript 就能跑起来说坑多也是真多数据存进去变成[object Object]、按 id 删一条结果整个列表没了、开两个标签页数据对不上、刷新之后列表莫名其妙空了——这些问题我在项目里基本都踩过一遍。它属于浏览器本地存储里最常被拎出来用的那一个语法门槛低到新手也能当天上手但要用得稳、用得久光会那几个 API 远远不够。这篇东西适合三类人看刚学 JavaScript 基础、第一次接触浏览器本地存储的同学已经会用 localStorage 但总在删除和修改环节翻车的开发者以及想把项目里散落各处的本地读写逻辑收拢成一套可维护封装的工程师。我会从定位讲到封装从单条数据的增删改讲到按 id 批量删除的遍历陷阱中间穿插能直接复制粘贴的代码和我在真实项目里总结出来的避坑清单。1. 先把 localStorage 的定位钉死1.1 它到底解决了什么问题做过前端的人大概都遇到过这种需求用户勾了一堆筛选条件、填了一半的表单、切到了深色主题结果手一抖刷新页面全部回到初始状态只能骂骂咧咧重新弄一遍。localStorage 存在的意义就是接住这类场景。它挂在window对象上属于 Web Storage 规范的一部分数据以键值对形式躺在浏览器本地关标签页、关浏览器、重启电脑数据都还在除非用户主动清理浏览数据或者代码里把它删掉。跟 sessionStorage 比后者的生命周期只到标签页关闭跟 Cookie 比它不会随着每一次 HTTP 请求被带到服务端容量也宽裕得多。它本质上就是浏览器给你开的一个安静的本地抽屉不打扰服务端也不打扰用户。落到具体开发场景我用它最多的有这么几类。第一类是 UI 偏好主题色、列表密度、侧边栏是否展开、表格列宽这类数据体积极小、读取频繁、丢了也不影响业务正确性是 localStorage 的绝对舒适区。第二类是草稿暂存长表单写到一半误关页面下次打开能恢复个七七八八体验差距非常直观。第三类是轻量业务缓存城市列表、字典项、上一次浏览到的位置避免每进一次页面就发一次请求。第四类是登录态的辅助标记但这里要打个问号——真正的凭证类信息不建议往这里塞原因后面细说。它不适合的地方同样明确数据量大超过几 MB、写入频繁比如滚动监听里每帧写一次、结构复杂需要索引查询、包含敏感内容。这四条是红线后面会逐条展开讲为什么。我习惯在动手写第一行存储代码之前先问自己一句这份数据丢了会不会影响业务如果会那它就不该只待在 localStorage 里服务端得有一份如果不会那才是它的地盘。定位钉死了后面写代码的时候才不容易乱来。1.2 几种浏览器存储方案的横向对比新手最容易犯的错是手上只有一把锤子看什么都是钉子。浏览器其实给了好几套存储方案选错了后期改起来很痛。下面这张表是我自己整理后贴在工位上的版本选型时扫一眼就能定维度CookiesessionStoragelocalStorageIndexedDB典型容量约 4KB约 5MB约 5MB通常几百 MB 起生命周期可设置过期时间标签页关闭即清长期保留直到主动删长期保留直到主动删是否随请求发送是每次同源请求都带否否否跨标签页可见是否是是API 风格字符串拼接同步同步异步、事务化能存的数据类型字符串字符串字符串结构化克隆可存对象、二进制适合的场景服务端需要读的小标记单次会话内的临时状态偏好设置、小缓存、草稿大数据量、离线数据、需要索引查询这张表里有两个点值得多说两句。第一是容量5MB 这个数字是按 UTF-16 字符估算的不同浏览器给的实际配额不完全一样中文、emoji 这类字符占的空间比拉丁字母更多所以别把 5MB 当成精确的可用空间写满的边缘地带很容易触发配额异常。第二是 API 风格localStorage 是同步的这是它上手快的原因也是它在大数据量下卡死主线程的原因。IndexedDB 异步、事务、能建索引代价是代码复杂度陡增一个小功能用不着上这个重武器。我的经验判断标准很土数据总量在一两百 KB 以内、读写频率不高、不需要查询条件localStorage 就够一旦开始想「我要按时间筛选」「我要搜关键词」那就是该换方案了。1.3 使用前必须知道的三条硬性约束第一条是同源策略。localStorage 按协议加域名加端口划分https://a.com和http://a.com是两个完全独立的仓库localhost:3000和localhost:5173也互不可见。开发时改个端口号数据就没了很多人第一次遇到会以为是代码写错了其实就是这个原因。更要知道的是同源策略本身不是安全边界——同源的第三方脚本能读到全部数据所以别把重要信息往里放。第二条是同步阻塞。setItem、getItem、removeItem都是同步执行的浏览器要读磁盘、要解析字符串全在主线程上完成。存个几 KB 的小对象毫无感觉但如果你把一个几 MB 的 JSON 塞进去再读出来解析页面会明显卡一下。我见过最典型的翻车是有人在滚动事件里记录阅读位置每帧写一次长列表页滚起来像放幻灯片。正确做法是节流比如 200 毫秒才写一次或者只在离开页面时写。第三条是只能存字符串。这不是「建议」是硬性限制。你传进去一个对象它会调用toString()结果就是字符串[object Object]取出来再也还原不回去。数字会变成字符串布尔值会变成true日期对象会变成一段日期格式的字符串。所以序列化这一步不是可选项是所有用法的前提下一节会专门拆开讲。2. 增删改的核心机制与最容易翻车的地方2.1 新增为什么一定要先序列化序列化这一步标准做法就是JSON.stringify配JSON.parse。写的时候先 stringify读的时候再 parse 还原。听起来像常识但真正会出问题的是那几个边角情况我一个一个说。undefined经过JSON.stringify之后会直接消失对象里的undefined属性会被整条丢掉数组里的undefined会变成null所以如果你的数据结构里允许某个字段为空要用null而不是undefined。日期对象序列化后变成字符串反序列化回来还是字符串不会自动变回Date要用的时候得自己new Date(str)转一下。BigInt遇到JSON.stringify会直接抛错因为它在 JSON 里没有对应类型。函数、正则、Map、Set也都存不下来会被丢掉或者变成空对象。再看读取这一侧。JSON.parse碰到不合法的字符串会抛异常。这条很要命如果某次写入中途出错或者早期版本写进去的结构和现在不兼容解析就会炸。如果这行代码在页面初始化的关键路径上整个页面可能直接白屏。所以我在所有读操作上都套了 try/catch解析失败就当数据不存在返回默认值同时把这条脏数据清掉避免它反复干扰。下面这个读的方法是通用写法可以直接抄function safeGet(key, fallback null) { try { const raw localStorage.getItem(key); if (raw null) return fallback; // 键不存在 return JSON.parse(raw); // 还原成原始类型 } catch (err) { console.warn([storage] 解析失败已清理脏数据:, key, err); localStorage.removeItem(key); // 防止反复报错 return fallback; } }注意raw null这个判断。getItem在键不存在时返回null而不是undefined这是个容易忽略的细节。另外别用if (!raw)来判断因为空字符串也是合法数据用!raw会把空字符串当成不存在逻辑就错了。写入侧加一个 try/catch 同样是必须的主要防两件事一是超出配额时抛出的异常二是无痕模式或某些隐私设置下写入直接失败。捕获之后给用户一个提示比让页面崩溃体面得多。2.2 键名设计单键整存还是多键分存这是新手在「新增」环节最容易埋雷的地方因为写的时候不觉得有问题删和改的时候才开始怀疑人生。举个例子你要做待办列表。方案 A 是把整个数组序列化后塞进一个键比如todo:list。方案 B 是每条待办单独一个键比如todo:item:1、todo:item:2。方案 A 的好处是读取只有一次 IO拿到就是完整列表排序、统计、全量渲染都很方便整体结构一致不太会出现半截数据。代价是每次改动都要「读全体、改局部、写全体」数据量大时比较费而且两个标签页同时操作会互相覆盖。方案 B 的好处是单条操作很轻改一条只写一个键。代价是列表页需要遍历所有键再拼装而且必须靠键名前缀来识别归属删除的时候要遍历匹配。热词里那个「根据 id 删除 localStorage 数据」说的就是这条路径。两种方案我列个对照对比项单键整存todo:list多键分存todo:item:{id}读取全列表一次搞定需要遍历所有 key修改单条读改写成本随总量增长只写一个键成本恒定删除单条过滤数组后整体回写直接 removeItem或遍历匹配前缀并发覆盖风险高整表回写会互相冲掉低互不干扰排序能力数组顺序自带依赖 id 或额外的时间字段适合规模几十到几百条数量多、单条更新频繁我的选择标准很简单条目数量在一两百以内并且经常需要整体展示就走方案 A条目多、单条更新频繁、或者本来就有服务端 id 一一对应就走方案 B。还有第三种混合玩法单条数据分存同时用一个索引键维护 id 顺序代价是索引和明细要保证一致出错概率变高除非确有必要我不太推荐新手一开始就这么做。无论选哪种键名都要加统一前缀比如项目名加业务名加数据版本形如myapp:todo:list:v1。裸着写list、data、temp这种键名早晚会跟同域下的其他模块撞车而且排查问题时根本认不出是谁写的。2.3 删除根据 id 删数据为什么不能直接 removeItemlocalStorage只有三个删除相关的口子removeItem(key)删一个键、clear()清空当前域下所有数据、以及配合key(index)遍历后逐个删。clear()是把当前域名下的所有存储全部抹掉如果这个域下只放了你自己的数据还好一旦有别的模块、别的 SDK、第三方统计脚本也在用同一个域你这一下就把人家的东西也清了。我在早期项目里干过一次这种蠢事测试环境里顺带把埋点 SDK 的标识也删了导致数据统计断了一整天。所以除非是「退出登录、清空所有本地状态」这种明确的整体清理场景clear()都不该出现在业务代码里。按 id 删除分两种情况。如果是方案 B 分存模式key 是拼接出来的从 id 到 key 是一次字符串拼接那删除就是一行function removeById(id) { localStorage.removeItem(myapp:todo:item:${id}); }真正麻烦的是分存模式下 id 不可信、只能按前缀删的情况比如数据是从服务端同步来的、id 规则变动过你只能遍历所有键找出属于这个业务的那一批。这时候有一个经典陷阱边遍历边删除会漏删。// 错误写法会漏 for (let i 0; i localStorage.length; i) { const k localStorage.key(i); if (k k.startsWith(myapp:todo:item:)) { localStorage.removeItem(k); // length 和索引当场变了 } }问题出在length是动态的。你删掉第 0 个键之后原来第 1 个键会补到索引 0 的位置而循环里的i已经加到了 1于是这一条被跳过了。数据的量越多漏得越狠。两种正确写法我一般用第一种先把键收集到数组里再统一处理// 正确写法一先收集再删除 function removeByPrefix(prefix) { const keys []; for (let i 0; i localStorage.length; i) { const k localStorage.key(i); if (k k.startsWith(prefix)) keys.push(k); } keys.forEach((k) localStorage.removeItem(k)); return keys.length; // 返回删了几条方便调试 } // 正确写法二倒序删除 function removeByPrefixReverse(prefix) { for (let i localStorage.length - 1; i 0; i--) { const k localStorage.key(i); if (k k.startsWith(prefix)) localStorage.removeItem(k); } }倒序之所以也行是因为删除只会让后面的索引前移而你已经往反方向走了所以不会漏。两种写法我都留了返回删除条数的习惯写单元测试或者调试时特别有用能一眼看出到底是「没匹配上」还是「匹配上了没删掉」。如果是方案 A 单键整存按 id 删除就是在数组里过滤掉那一条再写回去写法和修改几乎一样只是逻辑不同function removeFromList(id) { const list safeGet(myapp:todo:list:v1, []); const next list.filter((item) item.id ! id); localStorage.setItem(myapp:todo:list:v1, JSON.stringify(next)); return next; }这里有个细节要提醒filter里判断相等时id 的类型必须一致。如果 id 存在存储里是字符串1而你从按钮的dataset.id拿到的是字符串但某处又用了数字1去比!就会判定为不相等删除静默失败页面上什么反应都没有最难排查的就是这种。我习惯在删除函数入口统一做一次类型归一化const targetId String(id);后面所有比较都用字符串省掉一整类玄学问题。2.4 修改合并对象与数组更新的正确姿势修改的本质是三步读出旧数据、按要求变更、写回。问题几乎都出在第二步。如果存的是一个对象你想改其中一个字段最常见的直觉是重新组装一个完整对象塞回去但更稳的做法是基于旧数据做合并这样新增字段时不用同步改所有调用处。合并有几种写法各有各的脾气。Object.assign({}, oldObj, patch)是浅合并第一层键会覆盖但嵌套对象是整体替换而不是递归合并。展开运算符{ ...oldObj, ...patch }效果一样写起来更顺眼。热词里那个「JavaScript 合并两个对象」问的多半就是这个场景。这里必须强调浅合并的陷阱假设旧数据是{ theme: { color: blue, size: large } }你只想改颜色写了{ ...old, theme: { color: red } }那么size字段就彻底没了因为整个theme对象被替换掉了。正确写法得再展开一层{ ...old, theme: { ...old.theme, color: red } }。层级越深越容易出错所以我在实际项目里尽量让存储的数据保持扁平两层以上的嵌套基本不往 localStorage 里放需要的话就拆成两份数据分开存。如果存的是数组更新某个 id 的元素用map最直观没有匹配到的话原样返回天然安全function updateTodo(id, patch) { const list safeGet(myapp:todo:list:v1, []); const targetId String(id); let hit false; const next list.map((item) { if (String(item.id) ! targetId) return item; hit true; return { ...item, ...patch, updatedAt: Date.now() }; // 顺带记个更新时间 }); if (!hit) { console.warn([storage] 未找到目标数据:, targetId); return list; // 没命中就不写回避免无意义 IO } localStorage.setItem(myapp:todo:list:v1, JSON.stringify(next)); return next; }这段代码里有两个我坚持保留的习惯。一是命中标记hit没命中时不写回存储既省了一次 IO也能在控制台留下线索省得排查时怀疑人生。二是更新时顺带写入updatedAt时间戳后面做多标签页同步、或者做「最后修改时间」展示时都用得上属于成本极低但回报很高的提前投资。还有一个容易被忽略的坑数组元素如果是对象map里返回{ ...item, ...patch }创建的是新对象原来的引用不会被改动但如果你图省事直接item.status done然后回写整个数组在某些场景下会导致 React 等框架检测不到变化而不重新渲染。存储层虽然不直接管渲染但这种「就地改引用」的习惯传到业务层就是 bug从一开始就写成不可变更新更省心。3. 一套能直接抄的 CRUD 封装3.1 封装思路与分层设计原生 API 直接散在业务代码里有三个问题序列化逻辑到处都是、异常没人管、键名各写各的。我通常分两层来收拢。底层是通用工具层管序列化、异常、前缀、命名空间清理不关心业务语义上层是业务层针对具体实体提供list、add、update、remove、findById这些方法。这样底层可以原样复制到下一个项目业务层换实体名就能用。命名上我用一个统一前缀常量所有键都从它派生清空的时候也只会清自己这一片不动别人。3.2 基础工具层代码const PREFIX myapp:; const VERSION v1; const storage { // 拼出带前缀的完整键名业务层永远不写裸键 key(name) { return ${PREFIX}${name}:${VERSION}; }, set(name, value) { try { localStorage.setItem(this.key(name), JSON.stringify(value)); return true; } catch (err) { // 配额超限或写入被禁用都会走到这里 console.error([storage] 写入失败:, name, err); return false; } }, get(name, fallback null) { try { const raw localStorage.getItem(this.key(name)); return raw null ? fallback : JSON.parse(raw); } catch (err) { console.warn([storage] 读取失败已清理:, name, err); localStorage.removeItem(this.key(name)); return fallback; } }, remove(name) { localStorage.removeItem(this.key(name)); }, // 只清理本项目的键不会误伤同域下其他模块 clearAll() { const keys []; for (let i 0; i localStorage.length; i) { const k localStorage.key(i); if (k k.startsWith(${PREFIX}${VERSION})) keys.push(k); } keys.forEach((k) localStorage.removeItem(k)); return keys.length; }, // 调试用把本项目的数据全部打印出来 dump() { const result {}; for (let i 0; i localStorage.length; i) { const k localStorage.key(i); if (k k.startsWith(PREFIX)) { result[k] this.get(k.slice(PREFIX.length).replace(/:v\d$/, )); } } console.table(result); return result; }, };key(name)这个方法看着不起眼实际价值很高。业务代码里永远写storage.get(todoList)前缀和版本号由底层统一加将来要升级版本号只改一个常量全项目的键名跟着变老数据自然隔离不会出现新旧结构在一个键里打架的情况。3.3 业务层待办列表的增删改查const TODO_KEY todoList; const todoStore { list() { return storage.get(TODO_KEY, []); }, add(title) { const list this.list(); const item { id: ${Date.now()}_${Math.random().toString(36).slice(2, 7)}, title: String(title).trim(), done: false, createdAt: Date.now(), updatedAt: Date.now(), }; list.push(item); storage.set(TODO_KEY, list); return item; // 返回新对象方便调用方直接拿到 id }, update(id, patch) { const targetId String(id); let hit false; const next this.list().map((item) { if (String(item.id) ! targetId) return item; hit true; return { ...item, ...patch, updatedAt: Date.now() }; }); if (hit) storage.set(TODO_KEY, next); return hit; }, remove(id) { const targetId String(id); const next this.list().filter((item) String(item.id) ! targetId); storage.set(TODO_KEY, next); return next; }, findById(id) { const targetId String(id); return this.list().find((item) String(item.id) targetId) || null; }, };调用起来就是几行干净的业务代码const item todoStore.add(写完本地存储这篇笔记); todoStore.update(item.id, { done: true }); todoStore.remove(item.id); console.log(todoStore.list());id 的生成方式我用的是时间戳加随机串主要是为了避免同一毫秒内连续添加两条导致 id 重复纯时间戳在高频点击下真的会撞。如果你的数据来自服务端直接用服务端的 id 就行不要再本地生成一套两套 id 混用会带来无穷的映射麻烦。3.4 验证方式在控制台里把数据翻出来看写完存储代码一定要实际验证别凭感觉认为写对了。最直接的方式是打开开发者工具的 Application 面板左侧找到 Local Storage选中你的域名右边就是所有键值对值和类型一目了然。如果嫌面板切换麻烦控制台里有几条命令我几乎天天用localStorage.getItem(myapp:todoList:v1); // 看原始字符串确认真的是合法 JSON localStorage.length; // 一共存了多少个键 Object.keys(localStorage); // 所有键名列表查前缀有没有写乱 Object.entries(localStorage); // 全部键值对排查干扰项很快有一点必须提前说清楚面板里看到的永远是字符串哪怕你存的是数字。很多人在这里看到12就以为类型丢了其实是没有序列化前的那份数据参考。判断类型是否正确的唯一方式是在控制台执行JSON.parse(localStorage.getItem(...))看解析后的结果那才是业务层真正拿到的数据。4. 排查实录那些年踩过的本地存储坑4.1 高频问题速查表下面这张表是我和同事这几年攒下来的遇到问题先对着扫一遍通常能省掉半小时瞎找现象常见原因解决思路存进去变成[object Object]对象没做序列化就 setItem写入前JSON.stringify刷新后列表变空读取时解析抛异常被吞或者键名拼错读操作加 try/catch打印实际键名核对删一条结果整页数据都没了用了clear()改成removeItem或按前缀精确清理按前缀批量删除有残留边遍历边删导致索引位移先收集键到数组再统一删删除函数执行了但没效果id 类型不一致1 ! 1入口统一String(id)归一化偶发写入失败且无报错超出配额异常被忽略捕获异常并给用户反馈定期清过期数据两个标签页数据不一致只有内存状态没重新读存储监听storage事件刷新界面页面滚动卡顿高频写入阻塞主线程节流写入或改到pagehide时统一写无痕模式下直接报错存储被禁用try/catch 包裹降级到内存变量4.2 遍历删除为什么会漏上一节写了正确写法这里补充一下现场。我最早遇到这个问题是做一个「清空已完成的待办」功能测试数据只有三条怎么删都对上线后用户反馈「清空之后还剩几条」。原因就是数据量大了之后索引位移的影响被放大遍历跳过的条数随删除数量增加。当时的代码就是最自然的那种正序 for 循环本地测不出来是因为数据太少恰好没触到跳过的那一格。后来我把这段逻辑抽成了公共函数并把返回删除条数变成硬性要求任何调用处都能立刻验证删了几条这个习惯一直保留到现在。顺手说一个延伸坑localStorage.key(i)在某些环境下可能返回null虽然罕见但代码里如果不做空值判断k.startsWith就会直接抛错异常往上冒可能中断整个清理流程。所以我把if (k k.startsWith(...))这种写法当成肌肉记忆多敲几个字符换一份安稳。4.3 多标签页数据不同步用户在 A 标签页删了一条数据切到 B 标签页发现还在点一下报错「该数据不存在」体验很割裂。浏览器为此提供了storage事件只要同源的其他标签页改动了本地存储当前页就能收到通知。有几个特性必须记牢这个事件不会在做出改动的那个标签页里触发只通知「别人」key为null通常意味着调用了clear()oldValue和newValue都是字符串需要自己解析。window.addEventListener(storage, (e) { // 只关心自己这个业务的数据 if (e.key myapp:todoList:v1) { const next e.newValue ? JSON.parse(e.newValue) : []; renderTodoList(next); // 重新渲染 } // key 为 null 说明整片存储被清了 if (e.key null) { renderTodoList([]); } });实际用的时候要注意别写出死循环收到事件后重新渲染没问题但渲染过程里如果又去写存储就会在两个标签页之间来回触发。我的做法是把「响应同步」和「写存储」两条路径彻底分开收到事件只更新界面不回写。另外这个事件在高频写入时可能很密集配合节流会更稳。4.4 几条不太好写进文档的经验第一别在 localStorage 里放凭证据类信息。同源脚本能读到全部数据一旦页面引入了来源不明的第三方脚本这些内容就是敞开的。真要持久化登录态交给服务端下发的方式处理本地只存一些不敏感的标记位。第二写一个「数据体检」的小工具挂在开发环境里把键名、体积、最后修改时间列出来。很多隐蔽问题比如某个键在无人察觉中涨到了几 MB都是靠这类工具发现的。第三注意存储体积的估算方式。粗略可以用JSON.stringify(value).length拿字符数但它和实际占用的字节数不是一回事中文、emoji 差异更大。给自己留出余量别贴着上限用。第四键名带版本号这件事一开始会觉得啰嗦等数据结构变了需要迁移时你会感激当时的自己。迁移逻辑也很简单新版本键读不到数据时去读老版本键转换结构后写入新键再把老键删掉整个过程对业务透明。5. 再往前走一步几个实用扩展方向5.1 给数据加上过期时间localStorage 本身没有过期机制但很多业务数据其实是有时效的比如缓存的城市列表三天一更新或者一次性的活动状态只在一周内有效。做法是存的时候把数据包一层带上写入时间和有效期读的时候先判断是否过期。function setWithTTL(name, value, ttlMs) { storage.set(name, { v: value, exp: Date.now() ttlMs }); } function getWithTTL(name) { const box storage.get(name, null); if (!box) return null; if (Date.now() box.exp) { storage.remove(name); // 顺手清掉避免垃圾堆积 return null; } return box.v; }读取时顺手清理这一点很重要。如果只在读取时判断过期而不删除过期数据会一直占着配额时间久了照样能撑满。但要注意只有在数据被访问到才会被清理长期不访问的键依然躺在那里所以最好再配一个启动时的巡检遍历所有键把已经过期的批量删掉。5.2 用字符串做动作分发热词里有个「JavaScript 通过字符串调用函数」这个技巧在存储场景里其实挺好用。比如你要记录用户的最后几个操作做撤销或者要按配置执行不同的存储策略用一张方法映射表比一长串 if-else 清爽很多const actions { add: (payload) todoStore.add(payload.title), update: (payload) todoStore.update(payload.id, payload.patch), remove: (payload) todoStore.remove(payload.id), }; function dispatch(actionName, payload) { const fn actions[actionName]; if (typeof fn ! function) { console.warn([dispatch] 未知动作:, actionName); return null; } return fn(payload); }这么做的好处是扩展新动作只需要往表里加一行调用方传字符串就行配合存储在本地的一份「操作队列」还能实现简单的离线补发。但有一条安全底线字符串到函数的映射表必须是代码里写死的白名单绝不能拿用户输入的字符串去拼属性名或者用eval之类的动态执行方式那等于把任意代码执行的口子开给外部。5.3 什么情况下该换成 IndexedDB最后给个判断标准。当你开始遇到下面任意一条就该认真考虑换方案了单份数据超过一两 MB 且需要整体读写需要在几百上千条记录里按条件筛选和排序存储里出现了图片、音视频片段这类二进制内容写入频率高到主线程能感觉到卡。IndexedDB 的学习曲线确实陡异步回调、事务边界、版本升级都要花时间理解很多团队会直接上封装库来降低门槛。但对大多数中小型项目来说把 localStorage 用好、用对前缀、做好异常和过期处理已经足够覆盖九成以上的本地存储需求了。我自己判断的依据一直是那句老话先把手上这把刀用明白再考虑换工具过早引入复杂方案带来的维护成本往往比它解决的问题更多。
延伸阅读

更多相关文章

2026/10/1 9:21:41

基于YOLO的手势检测应用设计:从训练到部署全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 9:21:41

USDT地址怎么选?Omni、ERC20、TRC20三种类型对比与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 10:31:45

关键字新闻爬虫实战:百度与今日头条数据采集入库全方案

简介:这是一份基于 Java 实现的新闻爬虫项目,覆盖百度新闻与今日头条两个信源,支持按关键字批量抓取新闻并写入数据库,适合需要采集新闻数据的 Java 开发者、数据分析人员或爬虫初学者参考。压缩包共 20 个文件,包含 1…

2026/10/1 10:31:45

AMD花82亿美元买世界模型,图什么?

导读: 一家以芯片闻名的公司,拟用约82亿美元的股票买下一家做「世界模型」的公司,还要让李飞飞出任首席科学家。AMD图的是什么?目前能确认的是交易安排和标的方向;技术怎么接入产品、钱怎样赚回来,仍要看后…

2026/10/1 10:31:45

Java面试中Redis缓存一致性怎么答?

面试官问缓存一致性,不是想听你背“先删缓存再更新数据库”或者“先更新数据库再删缓存”这两句话。他想知道的是:你知不知道这两种顺序在并发场景下分别会出什么问题,以及你实际项目里怎么选、怎么兜底。先更新数据库,再删除缓存…

2026/10/1 10:31:45

溶解氧时间序列预测:LSTM与EEMD-LSTM完整源码实战

简介:面向溶解氧浓度时序预测的深度学习项目,源自个人期末大作业并获97分,含完整可运行源码与全部实验数据,适合正在做课程设计或期末作业的计算机相关专业学生,也适合希望实战时序预测的进阶学习者。项目覆盖从数据读…

2026/10/1 10:26:45

内容平台雷达系统:实时流量监测与规则变化预警

1. 这个项目到底是什么“PLFM_RADAR”这个代号,最初是我们团队内部对“平台信号雷达”的简称——PLFM 是 Platform 的缩写,RADAR 就是雷达。项目核心就一句话:做一个持续运转的内容平台监测系统,实时感知平台规则变化、流量波动、…

2026/10/1 5:21:14

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑