# Lazy Loading and Caching Geoid Grid Data in JavaScript: A Complete Implementation Guide

> Implement lazy loading and caching for geoid grid data in JavaScript. This guide details dynamic imports, promise caching, and result caching for efficient data handling.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: how-to-guide
- Published: 2026-08-30

---

**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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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.

```javascript
// 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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/geoid.js) (lines 49-57), the function throws a descriptive error if called before initialization:

```javascript
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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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:

```javascript
// 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:

```javascript
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:

```javascript
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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/militaryFlights.js)), use a Map to store per-aircraft values. For real-time rendering (like [`hud.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/hud.js)), cache the last computed value and only recompute when coordinates change significantly. This minimizes redundant grid lookups while keeping memory usage predictable.