Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungThe Model Got Better. Your Judgment Got Worse.(22.09.2026 um 03:02 Uhr)
Sichere ProgrammierungAIFeed - signed content permissions for AI web crawlers(22.09.2026 um 03:10 Uhr)
Sichere ProgrammierungThanks, glad you liked it!(22.09.2026 um 03:15 Uhr)
Sichere ProgrammierungA request for /.env shouldn't render your React app(22.09.2026 um 03:17 Uhr)
Sichere ProgrammierungAI-Agent Marketplaces Need Verifiable Delivery, Not More Listings(22.09.2026 um 03:20 Uhr)
AI & KI NachrichtenBeyond Bigger Models: Toward a Modular Cognitive Architecture(22.09.2026 um 03:21 Uhr)
Sichere ProgrammierungMasa Depan Manajemen Data: Mengenal Konsep Data Mesh yang Revolusioner(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungHow to Search Your Claude Code Conversation History(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungThe Model Got Better. Your Judgment Got Worse.(22.09.2026 um 03:02 Uhr)
Sichere ProgrammierungAIFeed - signed content permissions for AI web crawlers(22.09.2026 um 03:10 Uhr)
Sichere ProgrammierungThanks, glad you liked it!(22.09.2026 um 03:15 Uhr)
Sichere ProgrammierungA request for /.env shouldn't render your React app(22.09.2026 um 03:17 Uhr)
Sichere ProgrammierungAI-Agent Marketplaces Need Verifiable Delivery, Not More Listings(22.09.2026 um 03:20 Uhr)
AI & KI NachrichtenBeyond Bigger Models: Toward a Modular Cognitive Architecture(22.09.2026 um 03:21 Uhr)
Sichere ProgrammierungMasa Depan Manajemen Data: Mengenal Konsep Data Mesh yang Revolusioner(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungHow to Search Your Claude Code Conversation History(22.09.2026 um 03:22 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Angular Signals + Three.js: How We Hit 60fps Without Going Zoneless

How we optimized WebGL rendering in Angular using OnPush, Signals, and render-on-demand—without the experimental zoneless API This article assumes familiarity with Angular change detection and basic Three.js concepts. Context: P…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

How we optimized WebGL rendering in Angular using OnPush, Signals, and render-on-demand—without the experimental zoneless API



This article assumes familiarity with Angular change detection and basic Three.js concepts.






Context: Production Angular app (~500 DAU) with an interactive WebGL globe used in long-running sessions (30–90 minutes).



Problem: Our Three.js globe ran at 30fps because Zone.js triggered change detection on every requestAnimationFrame tick 60 unnecessary CD cycles per second. At 30fps, the globe felt laggy and drained laptop batteries.



A 3D scene should not re-render 60 times per second if nothing changed.



Solution: Combined ChangeDetectionStrategy.OnPush with Angular Signals and a render-on-demand pattern that only draws frames when the scene actually changes.



Ownership: The GlobeSceneService is the single owner of the render lifecycle and performance budget.




  • Start/stop animation loop on init/destroy


  • Expose requestRender() and requestFrameIfNeeded() APIs


  • Track metrics and expose health signals




This makes rendering behavior predictable, debuggable, and enforceable across the application.



Result: Consistent 60fps, 90% GPU reduction when idle, and zero Zone.js interference with the render loop.









The Problem: Zone.js Meets requestAnimationFrame



Three.js applications need a continuous animation loop. The standard pattern looks like this:




// Standard Three.js animation loop
function animate() {
requestAnimationFrame(animate); // Schedule next frame
controls.update(); // Update camera controls
renderer.render(scene, camera); // Render the scene
}
animate();





This works great—until you put it inside an Angular application.



The issue: Zone.js schedules requestAnimationFrame callbacks inside the Angular zone, which may trigger change detection depending on component strategy. At 60fps, that's 60 change detection cycles per second, even if nothing in your Angular templates changed.



The symptoms:





  • GPU usage at 20%+ even when idle (no user interaction)


  • FPS drops to 30fps on mid-range devices


  • Battery drain on laptops and mobile devices


  • UI jank when Angular templates try to update during renders







Why Not Just Use Experimental Zoneless?



Angular 18+ offers provideExperimentalZonelessChangeDetection(). It sounds perfect—no Zone.js, no problem. But there are real tradeoffs:





  1. It's experimental — API may change, limited community support


  2. Third-party libraries — Many Angular libraries assume Zone.js exists


  3. Migration effort — Existing apps need significant refactoring


  4. Testing complexity — Test utilities behave differently



We chose a hybrid approach: keep Zone.js for the benefits it provides (automatic change detection for UI interactions), but prevent it from interfering with Three.js.







The Solution: Three Interlocking Patterns



Our solution combines three patterns:





  1. OnPush change detection — Only update components when inputs/signals change


  2. Signal-based state — Fine-grained reactivity without Zone.js triggers


  3. Render-on-demand — Only render Three.js frames when the scene changes



Let's look at each one.







Pattern 1: OnPush Everything



Every component uses ChangeDetectionStrategy.OnPush:



// src/app/pages/globe/globe.ts

import {
Component,
ChangeDetectionStrategy,
signal,
computed,
effect
} from '@angular/core';

@Component({
selector: 'app-globe',
standalone: true,
templateUrl: './globe.html',
changeDetection: ChangeDetectionStrategy.OnPush, // ← Critical
})
export class GlobePage {
// Component implementation
}





With OnPush, Angular only checks the component when:




  • An @Input() reference changes

  • A signal the template reads updates

  • An event handler in the template fires

  • You manually call ChangeDetectorRef.markForCheck()



Crucially, requestAnimationFrame callbacks don't trigger OnPush components.







Pattern 2: Signals for Reactive State



Instead of RxJS BehaviorSubjects or plain properties, we use Angular Signals throughout:



// src/app/pages/globe/services/globe-scene.service.ts

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class GlobeSceneService {
// Reactive state with signals
readonly isInitialized = signal(false);
readonly cameraInteractionState = signal<boolean>(false);

// Private mutable state for Three.js (not reactive)
private scene!: Scene;
private camera!: PerspectiveCamera;
private renderer!: WebGLRenderer;
private controls!: OrbitControls;

// Animation state
private needsRender = true;
private isAnimating = false;
}





Signals provide:





  • Automatic template updates — Templates re-render when signals they read change


  • Fine-grained reactivity — Only affected components update, not the whole tree


  • No subscription management — Unlike RxJS, no need to unsubscribe







Pattern 3: Render-on-Demand



This is the core optimization: instead of rendering every frame, we only render when something changes.



Always stop the animation loop on component or service destroy using DestroyRef.onDestroy() (services) or ngOnDestroy() (components) to prevent duplicate render loops during navigation.



// src/app/pages/globe/services/globe-scene.service.ts

@Injectable({ providedIn: 'root' })
export class GlobeSceneService {
// Animation state
private animationId?: number;
private lastFrameTime = 0;
// Soft cap at ~60fps (used for throttling, not locking. 1000ms / 60)
private readonly frameInterval = 16.67;
private needsRender = true;
private isAnimating = false;


// Start the animation loop
startAnimation(): void {
if (this.isAnimating) return;
this.isAnimating = true;
this.animate();
}


// Stop the animation loop
stopAnimation(): void {
if (this.animationId !== undefined) {
cancelAnimationFrame(this.animationId);
this.animationId = undefined;
this.isAnimating = false;
}
}

// In GlobeSceneService (service owns render lifecycle)
private readonly destroyRef = inject(DestroyRef);

constructor() {
// Ensure we stop the loop when the service is destroyed
this.destroyRef.onDestroy(() => this.stopAnimation());
}



private animate(currentTime: number = 0): void {
this.animationId = requestAnimationFrame((time) => this.animate(time));

// Frame throttling: skip if called too soon
const deltaTime = currentTime - this.lastFrameTime;
if (deltaTime < this.frameInterval) {
return; // Skip this frame
}

const deltaTimeSeconds = deltaTime / 1000.0;
this.lastFrameTime = currentTime;

// Update OrbitControls (may set needsRender via 'change' event)
this.controls.update();

// Check for active animations (e.g., bird migrations)
const hasActiveMigrations =
this.globeMigrationService.isInitialized() &&
this.globeMigrationService.hasActiveAnimations();

// Animate migration paths if active
if (hasActiveMigrations) {
this.globeMigrationService.animate(deltaTimeSeconds);
this.needsRender = true;
}

// ONLY RENDER IF SOMETHING CHANGED
if (this.needsRender || hasActiveMigrations) {
this.renderer.render(this.scene, this.camera);
this.needsRender = false; // Reset flag after render
}
}


requestRender(): void {
this.needsRender = true;
}
}





The key insight: the animation loop runs continuously, but renderer.render() only executes when needsRender is true.



Why not use RxJS for the animation loop?

You can model requestAnimationFrame with RxJS (e.g., an observable driven by animationFrameScheduler), but for hot, imperative rendering loops the extra abstraction adds overhead and complexity. Three.js expects imperative control, and an imperative loop avoids the costs of Observable allocation, scheduling, and operator chains on each tick. We use RxJS for state and event streams and keep the render loop imperative for minimal overhead.







Triggering Renders: The Event-Driven Approach



needsRender is set explicitly in response to real scene changes:





1. Camera Movement (OrbitControls)





// src/app/pages/globe/services/globe-scene.service.ts

private setupControls(): void {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);

// Configure controls
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 3;
this.controls.maxDistance = 15;

// Track interaction state for UI
this.controls.addEventListener('start', () => {
this.cameraInteractionState.set(true); // Signal update for UI
});

this.controls.addEventListener('end', () => {
this.cameraInteractionState.set(false);
});

// CRITICAL: Request render when camera moves
this.controls.addEventListener('change', () => {
this.needsRender = true; // ← Triggers actual render
});
}







2. User Interactions in Components





// src/app/pages/globe/globe.ts

@Component({...})
export class GlobePage {
private globeSceneService = inject(GlobeSceneService);

// When user selects a country
onCountryClick(country: string): void {
this.selectedCountry.set(country);
this.applyCountryHighlight(country);
this.globeSceneService.requestRender(); // ← Request render
}

// When quiz state changes
constructor() {
effect(() => {
const selectedCandidate = this.quizStateService.selectedCandidate();
const gameState = this.quizStateService.gameState();

if (!selectedCandidate || gameState === 'idle' || gameState === 'ended') {
this.quizIntegration.clearQuizCandidateHighlight();
this.globeSceneService.requestRender(); // ← Request render
}
});
}
}









Signals + Effects for Angular-Three.js Bridge



Angular Signals and effect() create a clean bridge between Angular's reactive system and Three.js's imperative API:



// src/app/pages/globe/globe.ts

@Component({...})
export class GlobePage {
// Angular signals for UI state
protected readonly selectedCountry = signal<string | null>(null);
protected readonly isLoading = signal(false);

// Computed values for templates
protected readonly migrationCardData = computed<readonly MigrationCardData[]>(() => {
const activePaths = this.migrationState.activePaths();
const migrations = this.migrationState.migrations();
const species = this.migrationState.species();

return activePaths.slice(0, 3).map((activePath) => {
const migration = migrations.find(m => m.id === activePath.migrationId);
const speciesData = species.find(s => s.id === migration?.speciesId);

return {
speciesName: speciesData?.commonName ?? 'Unknown',
scientificName: speciesData?.scientificName ?? '',
distance: migration?.totalDistance ?? 0,
// ... more computed fields
};
});
});

constructor() {
// Effect: Sync Angular state → Three.js scene
effect(() => {
const selectedIds = this.countrySelectionService.selectedCountries();

// Update Three.js texture based on Angular signal
if (this.countryIdTextureService.getSelectionMaskTexture()) {
this.countryIdTextureService.updateSelectionMask(selectedIds);
// Note: updateSelectionMask internally calls requestRender()
}
});
}
}





The effect() function watches signals and runs whenever they change. This is how Angular state flows into Three.js without Zone.js interference.







App Configuration: Event Coalescing



We keep Zone.js but optimize it with event coalescing:



// src/app/app.config.ts

import {
ApplicationConfig,
provideZoneChangeDetection,
} from '@angular/core';

export const appConfig: ApplicationConfig = {
providers: [
// Keep Zone.js, but coalesce events for better performance
provideZoneChangeDetection({ eventCoalescing: true }),

// ... other providers
],
};





eventCoalescing: true batches multiple synchronous events into a single change detection cycle. This reduces CD overhead without removing Zone.js entirely.







Performance Results





Before Optimization

































Metric Value Notes
FPS 30fps Zone.js CD overhead
GPU Idle 20%+ Continuous rendering
CD Cycles 60/sec Every rAF triggers CD
Battery Poor Constant GPU/CPU usage




After Optimization

































Metric Value Improvement
FPS 60fps 2x faster
GPU Idle 2-5% 90% reduction
CD Cycles On-demand ~99% reduction
Battery Excellent Only active when needed




How to Measure





// Add to your scene service for debugging
private logPerformance(): void {
const info = this.renderer.info;
console.log({
drawCalls: info.render.calls,
triangles: info.render.triangles,
geometries: info.memory.geometries,
textures: info.memory.textures,
});
}





In production, we collect FPS, frame time, draw calls, and memory usage into a lightweight performance monitor backed by signals. This lets us surface real-time health indicators in the UI and catch regressions early:



readonly currentFPS = computed(() => this.metrics().fps);
readonly isPerformanceGood = computed(() => this.metrics().fps >= 55);









Additional Optimizations (Optional)




  • Adaptive geometry: Dynamically reduce sphere segments on low-memory or mobile devices (48–128 segments based on navigator.deviceMemory and hardwareConcurrency), cutting vertex count by up to 75%.


  • InstancedMesh for particles: Batch hundreds of migration particles into a single draw call instead of individual meshes:




new THREE.InstancedMesh(geometry, material, MAX_PARTICLES);









Tradeoffs and Gotchas





When This Pattern Works Best





  • 3D visualizations with intermittent user interaction


  • Data dashboards where renders are event-driven


  • Games with predictable update cycles


  • Existing Angular apps where full zoneless migration is risky





When to Consider Full Zoneless





  • New greenfield projects without third-party Zone.js dependencies


  • Extreme performance requirements where even coalesced Zone.js is too much


  • Simple apps without complex async operations





Common Mistakes





  1. Forgetting to call requestRender() after scene changes


  2. Calling requestRender() in a loop instead of once after batch updates


  3. Not using OnPush on components that read Three.js state


  4. Putting Three.js objects in signals — signals are for primitive/serializable state





The requestRender() Checklist



Call it after:




  • Changing material properties

  • Moving objects

  • Updating textures

  • Adding/removing scene objects

  • Changing lights



Don't call it:




  • Inside the animation loop (it's already handled)

  • Multiple times in the same synchronous block (once is enough)

  • For Angular-only state changes (signals handle that)







Key Takeaways





  1. You don't need experimental zoneless — OnPush + Signals + render-on-demand achieves 90% of the benefit


  2. Render-on-demand is the key — The animation loop runs, but actual rendering is conditional


  3. Signals bridge Angular and Three.js — Use effect() to sync reactive state to the scene


  4. Event coalescing helpsprovideZoneChangeDetection({ eventCoalescing: true })


  5. Measure everything — Use renderer.info and a performance monitor to validate improvements



The 3D Global Dashboard now runs at consistent 60fps with 2-5% GPU usage when idle. Users report dramatically better battery life, and the UI remains responsive even during complex animations without sacrificing Angular's developer ergonomics or maintainability.










GlobePlay - Interactive Geography, Bird Migration & Quiz



Explore 241 countries, trace bird migration paths, and test your geography knowledge on an interactive 3D globe. Built with Three.js and Angular.



favicon
globeplay.world



Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Angular Signals + Three.js: How We Hit 60fps Without Going Zoneless

Thematisch verwandte Begriffe: Angular, Signals, Threejs, 60fps · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-49449 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick