Lazy Loading and Caching Geoid Grid Data in JavaScript: A Complete Implementation Guide
The gods-eye-view repository implements a robust lazy-loading pattern that defers loading the 2.7 MB EGM96 geoid grid until runtime, using dynamic imports with single-shot promise caching and multi-layered result caching to eliminate redundant calculations.
The EGM96 geoid model is essential for converting between ellipsoidal and orthometric heights, but bundling its full grid data with your JavaScript application bloats initial load times. According to the bilawalsidhu/gods-eye-view source code, you can solve this by isolating the grid in a separate chunk and loading it on-demand only when altitude conversions are actually requested.
Why Lazy Load the EGM96 Geoid Grid?
The EGM96 grid represents a significant static asset—approximately 2.7 MB of binary data. Including this in your main Vite or Webpack bundle increases initial download size and parse time for users who might never trigger a height conversion. By deferring this load, you improve Time to Interactive (TTI) while maintaining full geoid correction capabilities when needed.
The Core Implementation Pattern
The repository follows a five-stage pipeline: dynamic import → single-shot promise → guarded API → local caching → fallback path.
Dynamic Import with Single-Shot Promise
In src/data/geoid.js, the ensureGeoidReady() function implements a lazy-loading singleton pattern. It stores the import promise in a module-level variable, ensuring the heavy egm96-universal package loads exactly once regardless of how many times the function is called.
// src/data/geoid.js – lines 32-38
export async function ensureGeoidReady() {
if (!readyPromise) {
readyPromise = import('egm96-universal').then((mod) => {
egm96Module = mod;
});
}
return readyPromise;
}
This pattern guarantees that subsequent invocations return the same resolved promise instantly, preventing race conditions and duplicate network requests.
Guarded API Pattern
Once loaded, the grid is accessed through geoidHeight(latDeg, lonDeg), which includes an explicit guard clause to prevent silent failures. As implemented in src/data/geoid.js (lines 49-57), the function throws a descriptive error if called before initialization:
export function geoidHeight(latDeg, lonDeg) {
if (!egm96Module) {
throw new Error(
'geoid.js: geoidHeight() called before ensureGeoidReady() resolved — ' +
'await ensureGeoidReady() first.'
);
}
return egm96Module.meanSeaLevel(latDeg, lonDeg);
}
This fail-fast approach forces consumers to properly await the initialization promise, ensuring deterministic results and eliminating undefined behavior from uninitialized state.
Caching Strategies for Performance
Beyond the initial lazy load, the repository implements multiple caching layers to avoid redundant geoid calculations.
Module-Level Promise Caching
The readyPromise variable in src/data/geoid.js acts as a simple promise cache. After the first resolution, all callers receive the cached module reference without re-triggering the dynamic import.
Application-Level Result Caching
Higher-level modules maintain their own geoid value caches. In src/data/militaryFlights.js, the code uses a _geoidNCache Map to store previously computed undulation values for specific aircraft positions. Similarly, src/data/terrainHeights.js implements a geoidCache Map that persists calculated offsets across terrain queries.
These caches populate after the grid becomes ready and are reused for every subsequent altitude conversion, eliminating the overhead of grid interpolation for repeated coordinates.
Per-Cell HUD Caching
For UI components requiring real-time updates, src/hud.js (lines 100-110) implements a lightweight spatial cache using _geoidCellKey and _geoidN variables. The HUD recomputes the geoid offset only when the camera moves to a new grid cell, reducing calculation frequency from every frame to only when spatial context changes.
Handling Conversions and Fallbacks
The repository provides helper functions that abstract the geoid math. The orthometricToEllipsoidal conversion combines the cached grid lookup with standard height transforms:
// Convert orthometric height (MSL) to ellipsoidal height (h = H + N)
import { orthometricToEllipsoidal } from './data/geoid.js';
import { ensureGeoidReady } from './data/geoid.js';
await ensureGeoidReady(); // Load the grid once
const orthoM = 15; // 15m above mean sea level
const lat = 37.7749;
const lon = -122.4194;
const ellipsoidalM = orthometricToEllipsoidal(orthoM, lat, lon);
console.log(`Ellipsoidal height: ${ellipsoidalM.toFixed(2)}m`);
If the dynamic import fails—due to network errors or missing files—the application gracefully continues without geoid correction, falling back to raw ellipsoidal heights. This ensures the HUD and flight tracking features remain functional even when offline.
Complete Usage Examples
Initialize the geoid system at application startup to preload the grid before any conversions are needed:
import { ensureGeoidReady, geoidHeight } from './data/geoid.js';
// 1️⃣ Preload the grid (runs once)
await ensureGeoidReady();
// 2️⃣ Query geoid undulation anywhere
const lat = 37.7749; // San Francisco
const lon = -122.4194;
const N = geoidHeight(lat, lon); // metres above WGS84 ellipsoid
console.log(`Geoid undulation: ${N.toFixed(2)}m`);
For components handling frequent updates, integrate with the HUD's optimized caching:
import { HUD } from './hud.js';
const hud = new HUD();
await hud.enable(); // Internally calls ensureGeoidReady()
function renderTick(camera) {
// Uses internal _geoidN caching for performance
const altMsl = hud.getAltMsl(camera.lat, camera.lon, camera.altEllipsoidal);
updateDisplay(altMsl);
}
Summary
- Defer heavy assets: Use dynamic imports to load the 2.7 MB EGM96 grid only when
ensureGeoidReady()is first called. - Cache promises: Store the import promise in a module-level variable to prevent duplicate loads and race conditions.
- Guard your API: Validate module initialization in
geoidHeight()before accessing the grid to ensure deterministic behavior. - Layer your caches: Combine module-level promise caching, application-level result Maps, and spatial cell caching for optimal performance.
- Fail gracefully: When the grid fails to load, continue with ellipsoidal heights rather than crashing the application.
Frequently Asked Questions
How does ensureGeoidReady() prevent loading the geoid grid multiple times?
The function checks a module-level readyPromise variable before importing. If the promise exists, it returns the existing promise rather than creating a new dynamic import. This ensures the egm96-universal module loads exactly once, regardless of how many components call the function.
What happens if I call geoidHeight() before the grid loads?
The function throws an explicit error indicating that ensureGeoidReady() must be awaited first. This guard clause prevents silent failures where the code would otherwise attempt to call methods on an undefined module reference.
Can the application work without the geoid grid?
Yes. If the dynamic import fails due to network issues or file unavailability, the code continues with a fallback to raw ellipsoidal heights. While this reduces altitude accuracy, it maintains core functionality for flight tracking and HUD display without blocking the user interface.
Where should I cache geoid results in my own application?
Follow the repository's pattern: cache at the consumption point. For batch processing (like militaryFlights.js), use a Map to store per-aircraft values. For real-time rendering (like hud.js), cache the last computed value and only recompute when coordinates change significantly. This minimizes redundant grid lookups while keeping memory usage predictable.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →