[{"data":1,"prerenderedAt":1097},["ShallowReactive",2],{"\u002Fblog\u002Fcesium-indexeddb-resource-cache":3,"\u002Fblog\u002Fcesium-indexeddb-resource-cache-surround":1086},{"id":4,"title":5,"author":6,"body":10,"date":1077,"description":1078,"extension":1079,"image":1080,"meta":1081,"minRead":137,"navigation":236,"path":1082,"seo":1083,"stem":1084,"__hash__":1085},"blog\u002Fblog\u002Fcesium-indexeddb-resource-cache.md","Cesium 资源请求的 IndexedDB 离线缓存实现",{"name":7,"avatar":8},"Kevin",{"src":9,"alt":7},"\u002Favatar.jpg",{"type":11,"value":12,"toc":1068},"minimark",[13,17,21,24,36,39,46,49,52,55,58,78,81,84,91,94,268,271,274,552,562,709,712,1022,1025,1028,1031,1043,1046,1049,1052,1055,1061,1064],[14,15,16],"h2",{"id":16},"这篇文章解决什么问题",[18,19,20],"p",{},"Cesium 场景里的影像、地形和 3D Tiles 都会拆成大量资源请求。第一次进入场景时，这些请求必须经过网络；第二次进入同一区域时，如果仍然重复下载同一批瓦片，用户会明显感到等待。",[18,22,23],{},"这个 Demo 想解决的是浏览器侧的资源复用：在 Cesium 发起请求前先查询 IndexedDB，命中就直接返回本地数据，未命中再走网络，并把响应写入本地缓存。这样固定场景、固定区域或常用视角的二次加载，可以减少对远程服务的依赖。",[18,25,26,27,31,32,35],{},"这里的缓存不是服务端 CDN，也不是完整离线包。它缓存的是 Cesium 请求回来的源数据，例如影像瓦片 Blob、地形瓦片 ArrayBuffer、",[28,29,30],"code",{},"tileset.json"," 和 ",[28,33,34],{},".b3dm\u002F.pnts"," 等 3D Tiles 内容。Cesium 后续解析、图片解码和 GPU 上传仍然会发生。",[14,37,38],{"id":38},"核心难点",[18,40,41,42,45],{},"第一个难点是拦截层级。Cesium 内部有不同资源类型，但大量请求最终会经过 ",[28,43,44],{},"Cesium.Resource","。如果在 Viewer 创建之后才安装拦截器，早期的图层、地形或 tileset 请求就可能已经发出，所以缓存工具必须在资源加载前启用。",[18,47,48],{},"第二个难点是响应类型还原。影像适合缓存为 Blob，地形和 3D Tiles 更适合保留 ArrayBuffer 或 JSON 原始结构。命中缓存后返回的数据必须和 Cesium 原始请求期望一致，否则内部解析链会被破坏。",[18,50,51],{},"第三个难点是缓存范围。IndexedDB 命中不等于一定更快。小图片、SkyBox、Cesium 内置静态 JSON 等资源如果全部进库，反而会增加事务开销。因此这个 Demo 默认只缓存影像瓦片、地形瓦片和 3D Tiles。",[14,53,54],{"id":54},"实现思路",[18,56,57],{},"整体链路可以分成四步。",[18,59,60,61,63,64,67,68,67,71,67,74,77],{},"第一步，包装 ",[28,62,44],{}," 的 GET 请求。工具覆盖 ",[28,65,66],{},"fetchArrayBuffer","、",[28,69,70],{},"fetchBlob",[28,72,73],{},"fetchJson",[28,75,76],{},"fetchImage"," 等常见入口，把请求统一交给缓存管线处理。",[18,79,80],{},"第二步，为每个请求生成稳定 key。key 使用规范化 URL、响应类型、请求方法和 headers 组成的可读签名，保留 query 参数，因为瓦片行列号、层级、token 都是资源身份的一部分。",[18,82,83],{},"第三步，按响应类型读写 IndexedDB。命中时从本地恢复 Blob、ArrayBuffer 或 JSON；未命中时调用 Cesium 原始请求，成功后再写入 IndexedDB，并记录资源类型、体积、访问时间和命中次数。",[18,85,86,87,90],{},"第四步，控制缓存边界。写入后按 ",[28,88,89],{},"lastAccessedAt"," 做 LRU 淘汰，默认容量限制为 128MB；同时用资源类型过滤减少无关资源进库，让缓存收益集中在真正昂贵的瓦片和模型资源上。",[14,92,93],{"id":93},"技术流程图",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","%%{init: {'theme':'tokyo-night', 'themeVariables':{'primaryColor':'#7aa2f7'}}}%%\nflowchart TD\n    A([开始]) --> B[提前安装 Cesium.Resource 请求拦截器]\n    B --> C[拦截 fetchArrayBuffer fetchBlob fetchJson fetchImage]\n    C --> D[规范化 URL 方法 headers 和响应类型]\n    D --> E[生成稳定缓存 key]\n    E --> F{资源是否属于缓存目标}\n    F -->|否| G[直接调用 Cesium 原始网络请求]\n    F -->|是| H[查询 IndexedDB 缓存记录]\n    H --> I{是否命中缓存}\n    I -->|命中| J[更新访问时间和命中次数]\n    J --> K[按响应类型反序列化返回 Cesium]\n    I -->|未命中| L[调用原始网络请求]\n    L --> M{响应是否可序列化}\n    M -->|是| N[写入 IndexedDB 并记录元数据]\n    M -->|否| O[跳过写入直接返回结果]\n    N --> P[按 lastAccessedAt 执行 LRU 淘汰]\n    P --> Q([请求处理完成])\n    K --> Q\n    G --> Q\n    O --> Q\n\n    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n    style Q fill:#9ece6a,stroke:#9ece6a,color:#fff\n    style F fill:#e0af68,stroke:#e0af68,color:#1f2335\n    style I fill:#e0af68,stroke:#e0af68,color:#1f2335\n    style N fill:#bb9af7,stroke:#bb9af7,color:#fff\n","mermaid","",[28,102,103,111,117,123,129,135,141,147,153,159,165,171,177,183,189,195,201,207,213,219,225,231,238,244,250,256,262],{"__ignoreMap":100},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,110],{},"%%{init: {'theme':'tokyo-night', 'themeVariables':{'primaryColor':'#7aa2f7'}}}%%\n",[104,112,114],{"class":106,"line":113},2,[104,115,116],{},"flowchart TD\n",[104,118,120],{"class":106,"line":119},3,[104,121,122],{},"    A([开始]) --> B[提前安装 Cesium.Resource 请求拦截器]\n",[104,124,126],{"class":106,"line":125},4,[104,127,128],{},"    B --> C[拦截 fetchArrayBuffer fetchBlob fetchJson fetchImage]\n",[104,130,132],{"class":106,"line":131},5,[104,133,134],{},"    C --> D[规范化 URL 方法 headers 和响应类型]\n",[104,136,138],{"class":106,"line":137},6,[104,139,140],{},"    D --> E[生成稳定缓存 key]\n",[104,142,144],{"class":106,"line":143},7,[104,145,146],{},"    E --> F{资源是否属于缓存目标}\n",[104,148,150],{"class":106,"line":149},8,[104,151,152],{},"    F -->|否| G[直接调用 Cesium 原始网络请求]\n",[104,154,156],{"class":106,"line":155},9,[104,157,158],{},"    F -->|是| H[查询 IndexedDB 缓存记录]\n",[104,160,162],{"class":106,"line":161},10,[104,163,164],{},"    H --> I{是否命中缓存}\n",[104,166,168],{"class":106,"line":167},11,[104,169,170],{},"    I -->|命中| J[更新访问时间和命中次数]\n",[104,172,174],{"class":106,"line":173},12,[104,175,176],{},"    J --> K[按响应类型反序列化返回 Cesium]\n",[104,178,180],{"class":106,"line":179},13,[104,181,182],{},"    I -->|未命中| L[调用原始网络请求]\n",[104,184,186],{"class":106,"line":185},14,[104,187,188],{},"    L --> M{响应是否可序列化}\n",[104,190,192],{"class":106,"line":191},15,[104,193,194],{},"    M -->|是| N[写入 IndexedDB 并记录元数据]\n",[104,196,198],{"class":106,"line":197},16,[104,199,200],{},"    M -->|否| O[跳过写入直接返回结果]\n",[104,202,204],{"class":106,"line":203},17,[104,205,206],{},"    N --> P[按 lastAccessedAt 执行 LRU 淘汰]\n",[104,208,210],{"class":106,"line":209},18,[104,211,212],{},"    P --> Q([请求处理完成])\n",[104,214,216],{"class":106,"line":215},19,[104,217,218],{},"    K --> Q\n",[104,220,222],{"class":106,"line":221},20,[104,223,224],{},"    G --> Q\n",[104,226,228],{"class":106,"line":227},21,[104,229,230],{},"    O --> Q\n",[104,232,234],{"class":106,"line":233},22,[104,235,237],{"emptyLinePlaceholder":236},true,"\n",[104,239,241],{"class":106,"line":240},23,[104,242,243],{},"    style A fill:#7aa2f7,stroke:#7aa2f7,color:#fff\n",[104,245,247],{"class":106,"line":246},24,[104,248,249],{},"    style Q fill:#9ece6a,stroke:#9ece6a,color:#fff\n",[104,251,253],{"class":106,"line":252},25,[104,254,255],{},"    style F fill:#e0af68,stroke:#e0af68,color:#1f2335\n",[104,257,259],{"class":106,"line":258},26,[104,260,261],{},"    style I fill:#e0af68,stroke:#e0af68,color:#1f2335\n",[104,263,265],{"class":106,"line":264},27,[104,266,267],{},"    style N fill:#bb9af7,stroke:#bb9af7,color:#fff\n",[14,269,270],{"id":270},"关键代码解析",[18,272,273],{},"缓存 key 不是简单 hash，而是可读请求签名。这样在 DevTools 的 IndexedDB 面板里可以直接看到资源 URL、响应类型和方法，排查瓦片是否命中会方便很多。",[95,275,279],{"className":276,"code":277,"language":278,"meta":100,"style":100},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","function createCacheKey(method, responseType, normalizedUrl, headers = {}) {\n  const signature = {\n    url: normalizedUrl,\n    responseType,\n    method,\n    headers\n  }\n  const signatureText = JSON.stringify(signature)\n\n  if (signatureText.length \u003C= READABLE_KEY_MAX_LENGTH) {\n    return signatureText\n  }\n\n  return JSON.stringify({\n    url: `${normalizedUrl.slice(0, READABLE_KEY_URL_PREVIEW_LENGTH)}...`,\n    responseType,\n    method,\n    headers,\n    signatureHash: hashString(signatureText)\n  })\n}\n","js",[28,280,281,324,337,351,358,365,370,375,401,405,434,442,446,450,465,505,511,517,524,540,547],{"__ignoreMap":100},[104,282,283,287,291,295,299,302,305,307,310,312,315,318,321],{"class":106,"line":107},[104,284,286],{"class":285},"spNyl","function",[104,288,290],{"class":289},"s2Zo4"," createCacheKey",[104,292,294],{"class":293},"sMK4o","(",[104,296,298],{"class":297},"sHdIc","method",[104,300,301],{"class":293},",",[104,303,304],{"class":297}," responseType",[104,306,301],{"class":293},[104,308,309],{"class":297}," normalizedUrl",[104,311,301],{"class":293},[104,313,314],{"class":297}," headers",[104,316,317],{"class":293}," =",[104,319,320],{"class":293}," {})",[104,322,323],{"class":293}," {\n",[104,325,326,329,333,335],{"class":106,"line":113},[104,327,328],{"class":285},"  const",[104,330,332],{"class":331},"sTEyZ"," signature",[104,334,317],{"class":293},[104,336,323],{"class":293},[104,338,339,343,346,348],{"class":106,"line":119},[104,340,342],{"class":341},"swJcz","    url",[104,344,345],{"class":293},":",[104,347,309],{"class":331},[104,349,350],{"class":293},",\n",[104,352,353,356],{"class":106,"line":125},[104,354,355],{"class":331},"    responseType",[104,357,350],{"class":293},[104,359,360,363],{"class":106,"line":131},[104,361,362],{"class":331},"    method",[104,364,350],{"class":293},[104,366,367],{"class":106,"line":137},[104,368,369],{"class":331},"    headers\n",[104,371,372],{"class":106,"line":143},[104,373,374],{"class":293},"  }\n",[104,376,377,379,382,384,387,390,393,395,398],{"class":106,"line":149},[104,378,328],{"class":285},[104,380,381],{"class":331}," signatureText",[104,383,317],{"class":293},[104,385,386],{"class":331}," JSON",[104,388,389],{"class":293},".",[104,391,392],{"class":289},"stringify",[104,394,294],{"class":341},[104,396,397],{"class":331},"signature",[104,399,400],{"class":341},")\n",[104,402,403],{"class":106,"line":155},[104,404,237],{"emptyLinePlaceholder":236},[104,406,407,411,414,417,419,422,425,428,431],{"class":106,"line":161},[104,408,410],{"class":409},"s7zQu","  if",[104,412,413],{"class":341}," (",[104,415,416],{"class":331},"signatureText",[104,418,389],{"class":293},[104,420,421],{"class":331},"length",[104,423,424],{"class":293}," \u003C=",[104,426,427],{"class":331}," READABLE_KEY_MAX_LENGTH",[104,429,430],{"class":341},") ",[104,432,433],{"class":293},"{\n",[104,435,436,439],{"class":106,"line":167},[104,437,438],{"class":409},"    return",[104,440,441],{"class":331}," signatureText\n",[104,443,444],{"class":106,"line":173},[104,445,374],{"class":293},[104,447,448],{"class":106,"line":179},[104,449,237],{"emptyLinePlaceholder":236},[104,451,452,455,457,459,461,463],{"class":106,"line":185},[104,453,454],{"class":409},"  return",[104,456,386],{"class":331},[104,458,389],{"class":293},[104,460,392],{"class":289},[104,462,294],{"class":341},[104,464,433],{"class":293},[104,466,467,469,471,474,477,479,482,484,488,490,493,496,500,503],{"class":106,"line":191},[104,468,342],{"class":341},[104,470,345],{"class":293},[104,472,473],{"class":293}," `${",[104,475,476],{"class":331},"normalizedUrl",[104,478,389],{"class":293},[104,480,481],{"class":289},"slice",[104,483,294],{"class":331},[104,485,487],{"class":486},"sbssI","0",[104,489,301],{"class":293},[104,491,492],{"class":331}," READABLE_KEY_URL_PREVIEW_LENGTH)",[104,494,495],{"class":293},"}",[104,497,499],{"class":498},"sfazB","...",[104,501,502],{"class":293},"`",[104,504,350],{"class":293},[104,506,507,509],{"class":106,"line":197},[104,508,355],{"class":331},[104,510,350],{"class":293},[104,512,513,515],{"class":106,"line":203},[104,514,362],{"class":331},[104,516,350],{"class":293},[104,518,519,522],{"class":106,"line":209},[104,520,521],{"class":331},"    headers",[104,523,350],{"class":293},[104,525,526,529,531,534,536,538],{"class":106,"line":215},[104,527,528],{"class":341},"    signatureHash",[104,530,345],{"class":293},[104,532,533],{"class":289}," hashString",[104,535,294],{"class":341},[104,537,416],{"class":331},[104,539,400],{"class":341},[104,541,542,545],{"class":106,"line":221},[104,543,544],{"class":293},"  }",[104,546,400],{"class":341},[104,548,549],{"class":106,"line":227},[104,550,551],{"class":293},"}\n",[18,553,554,555,67,558,561],{},"资源类型判断决定了哪些请求能进缓存。这里把地形和 3D Tiles 放在图片判断之前，是为了避免 ",[28,556,557],{},".pnts",[28,559,560],{},".b3dm"," 或局部 JSON 被误归类成普通资源。",[95,563,565],{"className":276,"code":564,"language":278,"meta":100,"style":100},"function isDefaultCacheTarget(info) {\n  if (info.resourceType === 'terrain' || info.resourceType === '3d-tiles') {\n    return true\n  }\n\n  if (info.resourceType === 'imagery') {\n    return isImageryTileUrl(info.normalizedUrl)\n  }\n\n  return false\n}\n",[28,566,567,584,632,640,644,648,673,690,694,698,705],{"__ignoreMap":100},[104,568,569,571,574,576,579,582],{"class":106,"line":107},[104,570,286],{"class":285},[104,572,573],{"class":289}," isDefaultCacheTarget",[104,575,294],{"class":293},[104,577,578],{"class":297},"info",[104,580,581],{"class":293},")",[104,583,323],{"class":293},[104,585,586,588,590,592,594,597,600,603,606,609,612,615,617,619,621,623,626,628,630],{"class":106,"line":113},[104,587,410],{"class":409},[104,589,413],{"class":341},[104,591,578],{"class":331},[104,593,389],{"class":293},[104,595,596],{"class":331},"resourceType",[104,598,599],{"class":293}," ===",[104,601,602],{"class":293}," '",[104,604,605],{"class":498},"terrain",[104,607,608],{"class":293},"'",[104,610,611],{"class":293}," ||",[104,613,614],{"class":331}," info",[104,616,389],{"class":293},[104,618,596],{"class":331},[104,620,599],{"class":293},[104,622,602],{"class":293},[104,624,625],{"class":498},"3d-tiles",[104,627,608],{"class":293},[104,629,430],{"class":341},[104,631,433],{"class":293},[104,633,634,636],{"class":106,"line":119},[104,635,438],{"class":409},[104,637,639],{"class":638},"sfNiH"," true\n",[104,641,642],{"class":106,"line":125},[104,643,374],{"class":293},[104,645,646],{"class":106,"line":131},[104,647,237],{"emptyLinePlaceholder":236},[104,649,650,652,654,656,658,660,662,664,667,669,671],{"class":106,"line":137},[104,651,410],{"class":409},[104,653,413],{"class":341},[104,655,578],{"class":331},[104,657,389],{"class":293},[104,659,596],{"class":331},[104,661,599],{"class":293},[104,663,602],{"class":293},[104,665,666],{"class":498},"imagery",[104,668,608],{"class":293},[104,670,430],{"class":341},[104,672,433],{"class":293},[104,674,675,677,680,682,684,686,688],{"class":106,"line":143},[104,676,438],{"class":409},[104,678,679],{"class":289}," isImageryTileUrl",[104,681,294],{"class":341},[104,683,578],{"class":331},[104,685,389],{"class":293},[104,687,476],{"class":331},[104,689,400],{"class":341},[104,691,692],{"class":106,"line":149},[104,693,374],{"class":293},[104,695,696],{"class":106,"line":155},[104,697,237],{"emptyLinePlaceholder":236},[104,699,700,702],{"class":106,"line":161},[104,701,454],{"class":409},[104,703,704],{"class":638}," false\n",[104,706,707],{"class":106,"line":167},[104,708,551],{"class":293},[18,710,711],{},"请求管线采用 cache-first。命中后更新访问时间，未命中才走网络；网络结果可序列化时写入 IndexedDB。这里的重点是返回值必须保持 Cesium 期望的类型，而不是统一返回一种自定义结构。",[95,713,715],{"className":276,"code":714,"language":278,"meta":100,"style":100},"async requestWithCache(resource, responseType, networkLoader, options = {}) {\n  const info = this.createRequestInfo(resource, responseType, options.requestOptions)\n  const cacheState = this.shouldUseCache(resource, info)\n\n  if (!cacheState.cacheable) {\n    return networkLoader()\n  }\n\n  const cachedRecord = await this.store.get(info.key)\n\n  if (cachedRecord) {\n    const touchedRecord = await this.store.touch(cachedRecord)\n    return this.deserializeRecord(touchedRecord, responseType, options.imageOptions)\n  }\n\n  const result = await networkLoader()\n  await this.writeRecord(info, result, options.contentType)\n  return result\n}\n",[28,716,717,752,787,811,815,836,845,849,853,886,890,903,930,959,963,967,982,1011,1018],{"__ignoreMap":100},[104,718,719,722,725,728,730,732,734,737,739,742,745,748,750],{"class":106,"line":107},[104,720,721],{"class":331},"async ",[104,723,724],{"class":289},"requestWithCache",[104,726,727],{"class":331},"(resource",[104,729,301],{"class":293},[104,731,304],{"class":331},[104,733,301],{"class":293},[104,735,736],{"class":331}," networkLoader",[104,738,301],{"class":293},[104,740,741],{"class":331}," options ",[104,743,744],{"class":293},"=",[104,746,747],{"class":293}," {}",[104,749,430],{"class":331},[104,751,433],{"class":293},[104,753,754,756,758,760,763,766,768,771,773,775,777,780,782,785],{"class":106,"line":113},[104,755,328],{"class":285},[104,757,614],{"class":331},[104,759,317],{"class":293},[104,761,762],{"class":293}," this.",[104,764,765],{"class":289},"createRequestInfo",[104,767,294],{"class":341},[104,769,770],{"class":331},"resource",[104,772,301],{"class":293},[104,774,304],{"class":331},[104,776,301],{"class":293},[104,778,779],{"class":331}," options",[104,781,389],{"class":293},[104,783,784],{"class":331},"requestOptions",[104,786,400],{"class":341},[104,788,789,791,794,796,798,801,803,805,807,809],{"class":106,"line":119},[104,790,328],{"class":285},[104,792,793],{"class":331}," cacheState",[104,795,317],{"class":293},[104,797,762],{"class":293},[104,799,800],{"class":289},"shouldUseCache",[104,802,294],{"class":341},[104,804,770],{"class":331},[104,806,301],{"class":293},[104,808,614],{"class":331},[104,810,400],{"class":341},[104,812,813],{"class":106,"line":125},[104,814,237],{"emptyLinePlaceholder":236},[104,816,817,819,821,824,827,829,832,834],{"class":106,"line":131},[104,818,410],{"class":409},[104,820,413],{"class":341},[104,822,823],{"class":293},"!",[104,825,826],{"class":331},"cacheState",[104,828,389],{"class":293},[104,830,831],{"class":331},"cacheable",[104,833,430],{"class":341},[104,835,433],{"class":293},[104,837,838,840,842],{"class":106,"line":137},[104,839,438],{"class":409},[104,841,736],{"class":289},[104,843,844],{"class":341},"()\n",[104,846,847],{"class":106,"line":143},[104,848,374],{"class":293},[104,850,851],{"class":106,"line":149},[104,852,237],{"emptyLinePlaceholder":236},[104,854,855,857,860,862,865,867,870,872,875,877,879,881,884],{"class":106,"line":155},[104,856,328],{"class":285},[104,858,859],{"class":331}," cachedRecord",[104,861,317],{"class":293},[104,863,864],{"class":409}," await",[104,866,762],{"class":293},[104,868,869],{"class":331},"store",[104,871,389],{"class":293},[104,873,874],{"class":289},"get",[104,876,294],{"class":341},[104,878,578],{"class":331},[104,880,389],{"class":293},[104,882,883],{"class":331},"key",[104,885,400],{"class":341},[104,887,888],{"class":106,"line":161},[104,889,237],{"emptyLinePlaceholder":236},[104,891,892,894,896,899,901],{"class":106,"line":167},[104,893,410],{"class":409},[104,895,413],{"class":341},[104,897,898],{"class":331},"cachedRecord",[104,900,430],{"class":341},[104,902,433],{"class":293},[104,904,905,908,911,913,915,917,919,921,924,926,928],{"class":106,"line":173},[104,906,907],{"class":285},"    const",[104,909,910],{"class":331}," touchedRecord",[104,912,317],{"class":293},[104,914,864],{"class":409},[104,916,762],{"class":293},[104,918,869],{"class":331},[104,920,389],{"class":293},[104,922,923],{"class":289},"touch",[104,925,294],{"class":341},[104,927,898],{"class":331},[104,929,400],{"class":341},[104,931,932,934,936,939,941,944,946,948,950,952,954,957],{"class":106,"line":179},[104,933,438],{"class":409},[104,935,762],{"class":293},[104,937,938],{"class":289},"deserializeRecord",[104,940,294],{"class":341},[104,942,943],{"class":331},"touchedRecord",[104,945,301],{"class":293},[104,947,304],{"class":331},[104,949,301],{"class":293},[104,951,779],{"class":331},[104,953,389],{"class":293},[104,955,956],{"class":331},"imageOptions",[104,958,400],{"class":341},[104,960,961],{"class":106,"line":185},[104,962,374],{"class":293},[104,964,965],{"class":106,"line":191},[104,966,237],{"emptyLinePlaceholder":236},[104,968,969,971,974,976,978,980],{"class":106,"line":197},[104,970,328],{"class":285},[104,972,973],{"class":331}," result",[104,975,317],{"class":293},[104,977,864],{"class":409},[104,979,736],{"class":289},[104,981,844],{"class":341},[104,983,984,987,989,992,994,996,998,1000,1002,1004,1006,1009],{"class":106,"line":203},[104,985,986],{"class":409},"  await",[104,988,762],{"class":293},[104,990,991],{"class":289},"writeRecord",[104,993,294],{"class":341},[104,995,578],{"class":331},[104,997,301],{"class":293},[104,999,973],{"class":331},[104,1001,301],{"class":293},[104,1003,779],{"class":331},[104,1005,389],{"class":293},[104,1007,1008],{"class":331},"contentType",[104,1010,400],{"class":341},[104,1012,1013,1015],{"class":106,"line":209},[104,1014,454],{"class":409},[104,1016,1017],{"class":331}," result\n",[104,1019,1020],{"class":106,"line":215},[104,1021,551],{"class":293},[18,1023,1024],{},"地形开关的性能问题也来自同一个原则：不要让无关异步工作参与用户交互。关闭地形时只切到椭球地形，已经加载过的 World Terrain provider 保留下来；再次打开时复用 provider，而不是重建 Viewer 或重新走复杂初始化。",[14,1026,1027],{"id":1027},"效果与边界",[18,1029,1030],{},"这个方案在网络较慢、资源较大、用户反复进入同一场景时收益最明显。比如远程地形瓦片、大体积 3D Tiles、常用影像区域，二次加载可以减少大量网络等待。",[18,1032,1033,1034,31,1036,1038,1039,1042],{},"但 IndexedDB 不能跳过 Cesium 的后续工作。",[28,1035,560],{},[28,1037,557],{}," 命中后仍然要解析，图片 Blob 命中后仍然要解码，纹理和 buffer 仍然要上传 GPU。如果资源本来就是本地 ",[28,1040,1041],{},"\u002Fpublic"," 文件，或者浏览器 HTTP cache 已经很快，IndexedDB 反而可能因为事务和数据还原多出一点成本。",[18,1044,1045],{},"因此缓存范围比“全量缓存”更重要。这个 Demo 默认只缓存影像瓦片、地形瓦片和 3D Tiles，避免 SkyBox、内置资源和普通页面图片挤占容量，也避免它们在频繁请求时制造额外事务。",[18,1047,1048],{},"如果后续要做生产级离线能力，可以考虑 Service Worker + Cache API 承接请求级响应缓存，再用 IndexedDB 保存元数据、容量估算和 LRU 索引。前者更接近浏览器原生 HTTP 缓存，后者更适合做可控管理。",[14,1050,1051],{"id":1051},"小结",[18,1053,1054],{},"Cesium 资源缓存的核心不是“把所有请求都塞进 IndexedDB”，而是找到真正值得缓存的资源，并保证命中后返回的数据类型不破坏 Cesium 原有解析链。",[18,1056,1057,1058,1060],{},"这条链路可以概括为：提前安装 ",[28,1059,44],{}," 拦截器，生成稳定请求签名，按响应类型读写 IndexedDB，再用资源类型和 LRU 控制缓存边界。",[18,1062,1063],{},"性能优化也遵循同一个思路。缓存命中只省掉网络，不省掉解析；交互开关只应该切换必要状态，不应该重建整个 Viewer。把缓存范围和场景生命周期都收窄到真正需要的部分，二次加载体验才会稳定。",[1065,1066,1067],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":100,"searchDepth":113,"depth":113,"links":1069},[1070,1071,1072,1073,1074,1075,1076],{"id":16,"depth":113,"text":16},{"id":38,"depth":113,"text":38},{"id":54,"depth":113,"text":54},{"id":93,"depth":113,"text":93},{"id":270,"depth":113,"text":270},{"id":1027,"depth":113,"text":1027},{"id":1051,"depth":113,"text":1051},"2026-05-09","解析如何拦截 Cesium.Resource 请求，把影像瓦片、地形瓦片和 3D Tiles 写入 IndexedDB，并控制缓存范围与二次加载性能。","md","\u002Fimages\u002Fdemos\u002Fcesium-indexeddb-cache-ai-cover.png",{},"\u002Fblog\u002Fcesium-indexeddb-resource-cache",{"title":5,"description":1078},"blog\u002Fcesium-indexeddb-resource-cache","NcPAC3_1wXDUe-B1rcDpaSrFXQGjdLXLgkclQ_wX7bI",[1087,1092],{"title":1088,"path":1089,"stem":1090,"description":1091,"children":-1},"Cesium imageSubRegion 多动图闪烁问题排查","\u002Fblog\u002Fcesium-image-subregion-version-flicker-investigation","blog\u002Fcesium-image-subregion-version-flicker-investigation","记录一次把序列帧动图迁移到业务标绘库后出现多实例闪烁的排查过程，重点分析 Cesium 1.125 与 1.140 在 Billboard 子区域贴图机制上的差异。",{"title":1093,"path":1094,"stem":1095,"description":1096,"children":-1},"Cesium 离屏渲染 Framebuffer 实现","\u002Fblog\u002Fcesium-offscreen-render-framebuffer","blog\u002Fcesium-offscreen-render-framebuffer","解析如何在 Cesium 中自建 Framebuffer，把指定视角渲染成离屏纹理并读回为 ImageData。",1778657542552]