DEV Community

Zenovay
Zenovay

Posted on

Building a live 3D globe of real time web traffic with Three.js and Server Sent Events

3D globe of live web traffic

For the new Zenovay landing page I built a 3D globe that shows actual live visits from customer sites in close to real time. Dots glow and fade where visits happen.

For the new Zenovay landing page I built a 3D globe that shows actual live visits from customer sites in close to real time. Dots glow and fade where visits happen.

This post is a complete walkthrough of how it's built, in case you want to do something similar.

What you see vs what it actually is

What the visitor sees: a smooth spinning Earth with bright dots appearing in different countries every few seconds.

What it actually is:

  • A THREE.SphereGeometry with an Earth texture
  • A THREE.Points instance updated in place as events arrive
  • A custom GLSL fragment shader for the glow effect
  • Server Sent Events streaming batched data from Cloudflare Workers
  • Lazy initialization to keep LCP fast

The data flow

Visitor on customer site
  -> Tracking script POSTs event to Cloudflare Worker (sub 100ms)
  -> Worker pushes to Queue
  -> Consumer aggregates per region every 2s
  -> SSE stream pushes to landing page
  -> Three.js animates new dot
Enter fullscreen mode Exit fullscreen mode

The points system (the tricky part)

Points need to appear, glow, and fade. Re creating geometry every event is too expensive. Instead, allocate a fixed buffer and reuse slots.

buildPointsSystem() {
  this.maxPoints = 1000
  this.points = new Float32Array(this.maxPoints * 3)
  this.lifetimes = new Float32Array(this.maxPoints)
  this.cursor = 0

  this.pointsGeometry = new THREE.BufferGeometry()
  this.pointsGeometry.setAttribute('position', 
    new THREE.BufferAttribute(this.points, 3))
  this.pointsGeometry.setAttribute('lifetime', 
    new THREE.BufferAttribute(this.lifetimes, 1))
}

addPoint(lat, lng) {
  const [x, y, z] = latLngToVec3(lat, lng, 5.05)
  const i = this.cursor * 3
  this.points[i] = x
  this.points[i + 1] = y
  this.points[i + 2] = z
  this.lifetimes[this.cursor] = 1.0
  this.cursor = (this.cursor + 1) % this.maxPoints
  this.pointsGeometry.attributes.position.needsUpdate = true
}
Enter fullscreen mode Exit fullscreen mode

The glow shader

// Vertex shader
attribute float lifetime;
varying float vLifetime;

void main() {
  vLifetime = lifetime;
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  gl_PointSize = 8.0 * lifetime;
  gl_Position = projectionMatrix * mvPosition;
}

// Fragment shader
varying float vLifetime;

void main() {
  vec2 center = gl_PointCoord - vec2(0.5);
  float dist = length(center);
  if (dist > 0.5) discard;

  float intensity = 1.0 - smoothstep(0.0, 0.5, dist);
  vec3 color = mix(vec3(0.3, 1.0, 0.7), vec3(0.5, 1.0, 0.9), vLifetime);
  gl_FragColor = vec4(color, intensity * vLifetime);
}
Enter fullscreen mode Exit fullscreen mode

Coordinates: lat/lng to 3D

function latLngToVec3(lat, lng, radius) {
  const phi = (90 - lat) * (Math.PI / 180)
  const theta = (lng + 180) * (Math.PI / 180)
  return [
    -radius * Math.sin(phi) * Math.cos(theta),
    radius * Math.cos(phi),
    radius * Math.sin(phi) * Math.sin(theta)
  ]
}
Enter fullscreen mode Exit fullscreen mode

Performance notes

  • THREE.Points with BufferGeometry keeps GPU memory stable
  • Fixed size buffer avoids GC pressure
  • Pixel ratio capped at 2 (no point rendering 3x on retina)
  • Lazy initialization: globe loads after main content paint
  • 60fps on iPhone 12, ~45fps on a 5 year old budget Android

See the final result on the landing page: zenovay.com

If you want the full source as a template, drop a comment and I'll open source it.

Valerio

Top comments (0)