Cache OBJ/WRL models globally to prevent duplicate loading#663
Cache OBJ/WRL models globally to prevent duplicate loading#663omwanere wants to merge 5 commits intotscircuit:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
src/hooks/use-global-obj-loader.ts
Outdated
| if (typeof window === "undefined") return | ||
|
|
||
| const cleanUrl = url.replace(/&cachebust_origin=$/, "") | ||
| const cleanUrl = url.split("&cachebust_origin=")[0] ?? url |
There was a problem hiding this comment.
The nullish coalescing operator ?? won't prevent empty strings. If url is "&cachebust_origin=123", split()[0] returns "" (empty string), and since empty string is not nullish, it won't fall back to url. This results in cleanUrl = "", causing cache misses and fetch failures.
Fix:
const cleanUrl = url.split("&cachebust_origin=")[0] || urlUse || instead of ?? to handle empty strings, or add explicit check:
const parts = url.split("&cachebust_origin=")
const cleanUrl = parts[0] && parts[0].length > 0 ? parts[0] : url| const cleanUrl = url.split("&cachebust_origin=")[0] ?? url | |
| const cleanUrl = url.split("&cachebust_origin=")[0] || url |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
| if (!(result instanceof Error)) { | ||
| cache.set(cleanUrl, { promise, result }) | ||
| } |
There was a problem hiding this comment.
Failed loads are cached permanently: When loadAndParseObj() returns an Error, the cache entry remains as { promise, result: null } (set on line 103). All subsequent requests for this URL will return the cached error indefinitely. Transient failures (network issues, temporary server errors) can never recover without a page refresh.
// Fix: Don't cache errors, or add cache expiry
const promise = loadAndParseObj().then((result) => {
if (!(result instanceof Error)) {
cache.set(cleanUrl, { promise, result })
} else {
// Remove failed entry to allow retry
cache.delete(cleanUrl)
}
return result
})| if (!(result instanceof Error)) { | |
| cache.set(cleanUrl, { promise, result }) | |
| } | |
| if (!(result instanceof Error)) { | |
| cache.set(cleanUrl, { promise, result }) | |
| } else { | |
| // Remove failed entry to allow retry | |
| cache.delete(cleanUrl) | |
| } |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.
There was a problem hiding this comment.
Pull request overview
This PR introduces global caching for 3D model loading to avoid repeatedly fetching and parsing the same OBJ/WRL/STL/GLTF assets, addressing performance issues described in Issue #93 for circuits that reuse identical models.
Changes:
- Added a global
modelCachearoundload3DModelto cache parsed STL/OBJ/WRL/GLTF models and returnclone()d instances for each consumer. - Adjusted the WRL loading path to use the shared
loadVrmlhelper while integrating it into the new cache. - Updated
useGlobalObjLoaderto be more robust (SSR-safe guard, simplified cachebust handling, and cleaner cache access) while preserving its global OBJ/WRL cache semantics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/utils/load-model.ts |
Adds a global modelCache and clones cached STL/OBJ/WRL/GLTF models on reuse to avoid redundant network and parse work when components share the same model_*_url. |
src/hooks/use-global-obj-loader.ts |
Refines the global OBJ/WRL hook by normalizing URLs (removing cachebust_origin), guarding for non-browser environments, and simplifying cache result handling and effect cleanup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/utils/load-model.ts
Outdated
| // ✅ Global cache to prevent duplicate model loading | ||
| const modelCache = new Map<string, THREE.Object3D>() | ||
|
|
||
| export async function load3DModel(url: string): Promise<THREE.Object3D | null> { | ||
| // ✅ Cache HIT | ||
| if (modelCache.has(url)) { | ||
| return modelCache.get(url)!.clone() | ||
| } |
There was a problem hiding this comment.
modelCache is a process-wide Map with no eviction, so every distinct URL loaded via load3DModel will be retained for the lifetime of the page, which may increase memory usage over time in long-running sessions that touch many different models. If this becomes a concern, consider adding a way to clear the cache or bounding its size (e.g., LRU), in line with how you want caching to behave elsewhere in the app.
src/utils/load-model.ts
Outdated
| if (model) { | ||
| modelCache.set(url, model) | ||
| return model.clone() | ||
| } | ||
|
|
||
| return null |
There was a problem hiding this comment.
loadVrml is declared to always either return an Object3D or throw (see src/utils/vrml.ts:19-29), so the if (model) { ... } check and the return null in this WRL branch are effectively dead code. To keep this helper easier to reason about, consider removing the null-check and unreachable return null so WRL behaves like the other loaders (returning a model on success and rejecting on failure).
| if (model) { | |
| modelCache.set(url, model) | |
| return model.clone() | |
| } | |
| return null | |
| modelCache.set(url, model) | |
| return model.clone() |
rushabhcodes
left a comment
There was a problem hiding this comment.
Also you can join our discord server to get your pr review faster https://discord.com/invite/V7FGE5ZCbA
| if (modelCache.has(url)) { | ||
| return modelCache.get(url)!.clone() | ||
| } |
There was a problem hiding this comment.
Race condition: Multiple simultaneous requests for the same uncached URL will all trigger separate network requests and parsing operations. Unlike use-global-obj-loader.ts which caches the promise itself, this only checks for completed results.
What breaks: If 10 components mount simultaneously requesting the same model, all 10 will make separate network requests instead of sharing one.
Fix: Cache the loading promise, not just the result:
const modelCache = new Map<string, THREE.Object3D>()
const loadingPromises = new Map<string, Promise<THREE.Object3D | null>>()
export async function load3DModel(url: string): Promise<THREE.Object3D | null> {
if (modelCache.has(url)) {
return modelCache.get(url)!.clone()
}
if (loadingPromises.has(url)) {
const result = await loadingPromises.get(url)!
return result ? result.clone() : null
}
const promise = (async () => {
// ... existing loading logic ...
})()
loadingPromises.set(url, promise)
const result = await promise
loadingPromises.delete(url)
return result ? result.clone() : null
}Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.
|
Any update on my PR? |
/claim #93
Fix
Demo Video
Attached showing 1 cache miss + 9 cache hits for repeated resistors
Improve.3D.Model.Loading.-.Brave.2026-01-27.17-01-46.mp4