Article

Why Svelte

We chose Svelte for a simple reason: true performance.

Unlike React or Vue, which ship a runtime to the browser, Svelte compiles your code down to pure JavaScript. The result:

  • 47% less code than the React equivalent
  • Zero Virtual DOM — surgical updates to the real DOM
  • Simplicity — concise, modern syntax, similar to Vue
  • Native TypeScript with no extra configuration
These characteristics speed up development and reduce bugs.

Explicit Reactivity

// Reactive state — clear and predictable
let scrollY = $state(0);
let collapsed = $state(false);

// Derived values — recalculated automatically
let isVisible = $derived(scrollY > 100 && !collapsed);

// Effects with automatic cleanup
$effect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
});

Each primitive has a clear purpose. $state for data that changes, $derived for computations, $effect for side effects. No magic, no tricks.


Pattern: Progressive Disclosure

The psychological principle behind our navigation is progressive disclosure — revealing information gradually so as not to overwhelm the user.

Implementation: Collapsible Navbar

// The navbar collapses after scrolling 300px
let collapsed = $state(false);

$effect(() => {
  const updateScroll = (scroll: number) => {
    scrollY = scroll;
    collapsed = scroll > 300; // Progressive disclosure
  };
  // ...event subscription
});

Why does it work? When the user arrives, they see all the navigation options. Once they start scrolling (a signal that they found something interesting), we reduce distractions by collapsing the menu. This applies the selective attention principle — fewer visible options = greater focus on the content.

The Psychological Effect

According to Cialdini's research on commitment and consistency, once a user takes a micro-action (scrolling), they are more likely to continue in that direction. The collapsed navbar reinforces that decision: "You're already exploring — keep going."


Pattern: Immediate Feedback (Glow Effects)

One of the most important UX principles is immediate feedback. Every user action should have an instant visual response.

The Problem

Static borders are boring. They don't communicate that the site is "alive" or that it responds to the user.

The Solution: Borders that Follow the Cursor

// Global mouse position store
let mouseX = $state(0);
let mouseY = $state(0);

function handleMouseMove(e: MouseEvent) {
  pendingX = e.clientX;
  pendingY = e.clientY;

  // RAF throttling: maximum 60 updates/second
  if (rafId !== null) return;
  rafId = requestAnimationFrame(() => {
    mouseX = pendingX;
    mouseY = pendingY;
    rafId = null;
  });
}

The glow follows the cursor, creating a sense of direct connection between the user and the interface. This activates what neuromarketing calls embodied cognition — when the body (mouse movement) connects with the visual, engagement increases.

Optimization: Proximity Culling

function draw() {
  // Don't draw if the mouse is far away
  const isNear = Math.abs(mouseX - elementX) < 100;
  if (!isNear) return; // Saves GPU cycles

  // Only draw when the user is nearby
  const gradient = ctx.createRadialGradient(/*...*/);
  ctx.fill();
}

Smart trade-off: The effect only renders when the user is near the element. This preserves the visual magic without sacrificing performance across the rest of the page.


Pattern: Smooth Scroll and Perceived Quality

Native scrolling is functional but abrupt. Smooth scrolling communicates quality and attention to detail.

The Psychology of Motion

UX research shows that animations with exponential easing (gradual acceleration/deceleration) are perceived as more "natural" and professional. The human brain expects physical objects to have inertia.

// Exponential easing — feels "physical"
const easing = (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t));

lenisInstance = new Lenis({
  duration: 1.2,
  easing,
  smoothWheel: true,
  // Critical: don't break position:fixed on mobile
  syncTouch: false
});

Reactive Scroll

// Scroll state available globally
let scrollY = $state(0);
let scrollDirection = $state<'up' | 'down'>('down');

lenisInstance.on('scroll', (e) => {
  scrollY = e.scroll;
  scrollDirection = e.direction > 0 ? 'down' : 'up';
});

Other components can react to scrollDirection. For example, showing a "back to top" button only when the user is scrolling up — anticipating user needs.


Pattern: Server-Side Rendering and Perceived Speed

Perceived speed matters more than actual speed. A site that shows content immediately feels faster than one that shows a spinner, even if the total load time is the same.

Dynamic Blog with No Loading States

// Everything happens on the server
export const load: PageServerLoad = async ({ params }) => {
  const post = await getBlogPost(params.slug);

  // The client receives complete HTML, not JSON + render
  return { post };
};

// Fetching from S3 — no CMS cold starts
export async function loadBlogPostsServer(): Promise<BlogPost[]> {
  const command = new GetObjectCommand({
    Bucket: S3_BLOG_BUCKET,
    Key: 'blog-entries.json'
  });

  const response = await s3Client.send(command);
  return JSON.parse(await response.Body.transformToString()).posts;
}

Result: The user never sees a "Loading..." message. The page appears fully rendered because the server has already done all the work.

The Engagement Principle

In digital marketing, every second of wait time reduces conversion by ~7%. By eliminating loading states:

  1. We reduce friction — nothing interrupts the reading flow
  2. We increase credibility — a fast site is perceived as professional
  3. We improve SEO — Google rewards Core Web Vitals

Pattern: Canvas over CSS for High-Performance Effects

When you need effects that update 60 times per second, CSS isn't enough. Every gradient change forces a browser re-layout.

The Solution: Direct Canvas

function drawGlow(ctx: CanvasRenderingContext2D) {
  ctx.clearRect(0, 0, width, height);

  const gradient = ctx.createRadialGradient(
    mouseX, mouseY, 0,    // Center at the cursor
    mouseX, mouseY, 150   // 150px radius
  );

  gradient.addColorStop(0, 'rgba(235, 94, 40, 0.9)');
  gradient.addColorStop(0.4, 'rgba(235, 94, 40, 0.4)');
  gradient.addColorStop(1, 'rgba(235, 94, 40, 0)');

  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, width, height);
}

Canvas vs CSS:

  • CSS: Layout → Paint → Composite (3 phases)
  • Canvas: Paint only (1 phase)
For effects that update constantly, Canvas is ~3x more efficient.

Optimization: IntersectionObserver

A common problem with Canvas: requestAnimationFrame loops keep running even when the element is off-screen. On a page with 9+ canvases, this wastes CPU/GPU.

let isVisible = $state(false);
let observer: IntersectionObserver | null = null;

function startLoop() {
  if (animationId !== null) return;
  animationId = requestAnimationFrame(loop);
}

function stopLoop() {
  if (animationId !== null) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }
}

onMount(() => {
  observer = new IntersectionObserver(
    (entries) => {
      isVisible = entries[0].isIntersecting;
      if (isVisible && !isMobile) {
        startLoop();  // Only animate when visible
      } else {
        stopLoop();   // Pause when out of the viewport
      }
    },
    { rootMargin: '100px', threshold: 0 }
  );
  observer.observe(canvas);
});

The result: Canvases only consume resources when the user can actually see them. The 100px rootMargin ensures the animation starts smoothly before the element fully enters the viewport.


Pattern: Graceful Degradation on Mobile

Effects that work on desktop can destroy the experience on mobile. Limited battery, less powerful GPU, touch interaction instead of mouse.

Detection and Adaptation

The naive approach — adding a resize listener in every component — creates performance problems: 5+ redundant listeners, each causing layout thrashing.

The solution: Centralized Resize Handler

// src/lib/utils/resize.ts
const DEBOUNCE_DELAY = 150;
let subscribers = new Set<ResizeCallback>();
let debounceTimer: ReturnType<typeof setTimeout> | null = null;

function notifySubscribers(width: number, height: number) {
  subscribers.forEach(callback => callback(width, height));
  // ScrollTrigger refreshes once, not per component
  ScrollTrigger.refresh();
}

export function subscribeResize(callback: ResizeCallback): () => void {
  subscribers.add(callback);
  attachListener();
  // Immediate invocation with current dimensions
  callback(window.innerWidth, window.innerHeight);
  return () => {
    subscribers.delete(callback);
    if (subscribers.size === 0) detachListener();
  };
}

Now components simply subscribe:

import { subscribeResize, getDeviceTypeFromWidth } from '$lib/utils/resize';

let isMobile = $state(false);

onMount(() => {
  const unsubscribe = subscribeResize((width) => {
    isMobile = width < 768;
  });
  return unsubscribe;
});

// Disable expensive effects on mobile
const shouldRenderGlow = $derived(!isMobile && !disabled);

{#if shouldRenderGlow}
  <canvas bind:this={canvas} />
{:else}
  <!-- Static fallback: a simple border -->
  <div class="border-subtle"></div>
{/if}

The principle: On mobile, less is more. A simple, fluid experience beats a "complete" experience that lags.

Benefits of the centralized handler:

  • A single event listener for the entire app
  • 150ms debounce prevents layout thrashing
  • ScrollTrigger.refresh() runs once, not per component
  • Lazy initialization: the listener only exists if there are subscribers

Final Architecture

src/
├── lib/
│   ├── actions/
│   │   └── useLenis.svelte.ts    # Smooth scroll
│   ├── components/
│   │   ├── GlassNav.svelte       # Adaptive navbar
│   │   ├── GlowingBorder.svelte  # Canvas effects
│   │   └── BorderCanvas.svelte   # Canvas + IntersectionObserver
│   ├── config/
│   │   ├── animations.ts         # Animation configuration
│   │   └── animations.spec.ts    # Configuration tests
│   ├── utils/
│   │   ├── resize.ts             # Centralized resize handler
│   │   ├── resize.spec.ts        # Resize handler tests
│   │   ├── gsap.ts               # GSAP plugin registration
│   │   └── gsap.spec.ts          # GSAP utility tests
│   ├── stores/
│   │   └── mouse.svelte.ts       # Global cursor state
│   └── data/
│       └── blog.ts               # S3 data layer with cache
└── routes/
    ├── +layout.svelte            # SSR + Lenis init
    └── blog/[slug]/
        └── +page.server.ts       # Server-side data loading

Each piece has a clear purpose:

  • Actions: Reusable behaviors (scroll, reveal)
  • Components: UI with encapsulated logic
  • Config: Centralized configuration (animations, breakpoints)
  • Utils: Shared services (resize, GSAP, helpers)
  • Stores: Shared state across components
  • Data: Data access layer (S3, APIs)
  • Tests (.spec.ts): Co-located with the code they test

GSAP Centralization

Another pattern we implemented: registering GSAP plugins only once.

// src/lib/utils/gsap.ts
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';

let initialized = false;

export function initGSAP(): typeof gsap {
  if (!initialized && browser) {
    gsap.registerPlugin(ScrollTrigger);
    initialized = true;
  }
  return gsap;
}

// Auto-initialization on first import
if (browser) { initGSAP(); }

export { gsap, ScrollTrigger };

Components simply import from the centralized module:

// Before: every component did this
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger); // Redundant!

// After: one line, no redundancy
import { gsap, ScrollTrigger } from '$lib/utils/gsap';

Impact: We eliminated duplicate registerPlugin() calls across 7 components.


Performance Budget

OptimizationBeforeAfterImpact
Canvas animation loops9+ concurrentVisible only🔴 High
Resize listeners5+ per section1 centralized🟡 Medium
ScrollTrigger.refresh()Multiple per resize1 with debounce🟡 Medium
GSAP plugin registrationPer component1 on load🟢 Low

Conclusions

A website's architecture isn't just about technical performance — it's about how it feels to use it.

  1. Progressive disclosure reduces cognitive load
  2. Immediate feedback creates emotional connection
  3. Smooth scroll communicates quality and professionalism
  4. SSR eliminates friction from the experience
  5. Graceful degradation respects the user's context
  6. Smart centralization eliminates redundant work (resize, GSAP, animations)
Svelte gave us the tools to implement all of this with clean, performant code. But the architectural decisions came from understanding what makes a user want to stay — and from measuring the real impact of each optimization.


Want a site that combines solid architecture with real engagement? Let's talk.