Marketing site for a SaaS product company
Performance rescue
The problem
On mobile the largest paint landed at 21 seconds and the page weighed 21 MB. PageSpeed moved between 40 and 55.
What I found
The headline reveal used gsap.from(), which writes opacity 0 the moment the timeline is built, inside an overflow mask. Chrome drops a paint candidate that loads hidden, so the real one never counted. That was my own animation code. The hero video was 14 MB of HEVC that Chrome cannot decode.
What I changed
The headline paints on first load and reveals with a transform only. The video became H.264 with a fast start. Fonts became subset WOFF2, images WebP, and reCAPTCHA waits for the first form focus.
Largest contentful paint, mobile
3.0s
Down from 21.1 s
Page weight
5.8MB
Down from 21.2 MB
Total blocking time
95ms
Down from 310 ms
- Role
- Front-end developer
- Stack
- WordPress, GSAP, Three.js, Custom animated preloader
- Note
- PageSpeed Insights, live site, August 2026.
Site for an architecture and workplace-design consultancy
Polish plus speed
The problem
Page transitions, scroll-driven animation, smooth scroll and a physics interaction, on a site its owners edit themselves. That mix usually comes out fragile or slow.
What I decided
A custom theme with no page builder. Flexible content fields in place of the block editor, so the motion survives a site that keeps being edited.
What is still open
Mobile scores 84, not 99. The headroom left is plain work: minification, about 42 KiB of unused CSS, about 46 KiB of unused JavaScript and one redirect chain.
PageSpeed, desktop
99/ 100
Largest contentful paint, desktop
0.4s
Layout shift, mobile
0
- Role
- Front-end developer
- Stack
- WordPress, Vite + Sass, GSAP + ScrollTrigger, Lenis, Barba.js, Matter.js
- Note
- PageSpeed Insights, live site, August 2026. All motion switched on.
A halftone shader lab, my own project
Raw WebGL
The idea
Dot-field shaders on one full-screen canvas, with a panel to switch variants and tune them live. One variant raymarches a small 3D scene and draws it as halftone.
The contract
Every variant defines three things: how bright a dot is, where it moves, and how many neighbour cells a pixel has to search. Adding a variant is one entry. The search cost is visible in the code, not hidden.
What I have not done
The 3D scene is built from primitives, not a real model. Performance on high-density screens is capped, not profiled.
Dependencies
0
Shader variants
6
Device pixel ratio cap
2x
- Role
- Everything
- Stack
- WebGL, GLSL, No build step
- Note
- Open it on a desktop and try variants 1 to 3 first.
Vue and Nuxt work
The framework I work in every day. One client site and three projects of my own.
Releasd
Marketing site for a PR reporting SaaS
It started as maintenance of a Nuxt 2 site and became a full rebuild. Client since September 2025, still running.
Nuxt 4, Nuxt UI 4, Nuxt Content, TypeScript
Nuxt GSAP portfolio template
Open source template
Smooth scroll, page transitions driven by directives, an orchestrated entrance animation and a theme switch that morphs colour with GSAP.
Nuxt 4, GSAP, ScrollSmoother, TypeScript
Quorum
Group scheduling with no database server
Browsers sync with each other directly, and every write is signed with a per-device key. Unit tests plus Playwright runs that prove two browsers converge.
Nuxt 4, RxDB, WebRTC, Playwright
Pushup tracker
Peer-to-peer workout tracker
Offline first. Devices sync without a server in between, and the counter is an SVG animated with GSAP.
Nuxt 3, RxDB, WebRTC, GSAP
How this page is built
I have not shipped Angular in production. My daily stack is Vue and Nuxt with TypeScript. So I built this page in Angular 22.2.1 to show how my motion work sits in it.
- Signals, no zone.js
- The app is zoneless. State is signals, so a requestAnimationFrame loop or a scroll event never triggers change detection.
- Reduced motion as one signal
- A single service turns the OS setting into a signal. Every animated component reads it and reacts while the page is open.
- afterRenderEffect for scroll motion
- Each case study builds its ScrollTrigger timeline after render. The effect cleanup reverts it before a rebuild and on destroy.
- afterNextRender and DestroyRef for WebGL
- The canvas scene starts once in the browser and is disposed with its WebGL context when the component goes away.
- @defer for the 3D scene
- Three.js is a separate chunk that loads when the browser is idle. With no GPU, or under 30 fps, it steps aside for a still CSS pattern.
- Prerendered, no server
- The HTML is built ahead of time and hydrated. Without JavaScript, every word on this page is still here.
- Tested
- Unit tests cover the motion service and prove a case study leaves no ScrollTrigger behind.