Animating a Portfolio Without Wrecking It
Three animation libraries, one main thread, and the budget I set
A portfolio is allowed to show off. It is not allowed to drop frames doing it, and it is definitely not allowed to make a phone warm. This site runs a WebGL background, scroll-triggered reveals across every section, and a carousel, and the interesting work was not adding any of that. It was deciding what to cut.
01Setting a Budget First
I set two rules before writing any animation code, and they did more for the result than any optimization afterward. First, nothing animates a property that triggers layout. Transform and opacity are the only two properties in the budget, because they can be handled by the compositor without the browser recalculating positions for the rest of the page. Animating width, height, top, or margin forces layout on every frame, and on a page with a few hundred elements that is where the frames go. Second, nothing animates that the visitor cannot see. Scroll-triggered reveals fire once when an element enters the viewport and then stop observing it. An animation that keeps running for a section three screens up is spending frames on nothing. Both rules are easy to state and easy to break in a hurry, because the natural way to write a reveal is to animate height from zero, and it works fine on the machine you built it on. The budget exists so the decision is already made when you are tired and want to ship.
02Why Three Libraries
Carrying three animation libraries needs a defense, and mine is that they solve genuinely different problems. The first handles component-level motion: enter and exit transitions, scroll-triggered reveals, and gesture states. It is declarative, it understands the component lifecycle, and it makes the common case a prop rather than an effect. The second is for timeline work, sequences where several things need to happen in a specific order with precise offsets. Expressing that declaratively gets awkward quickly, and an imperative timeline is the right tool. The third is a small WebGL library driving the aurora background, which is a fragment shader and has nothing to do with the DOM at all. Where I was wrong was reaching for the timeline library for things the declarative one already did, which meant two ways to write a fade-in and no rule for choosing. I consolidated that. The honest accounting is that this is still more dependency weight than a portfolio needs, and if I were starting again I would try to do the sequencing work with the declarative library first and only add the second if it genuinely could not express it.
03The Background That Nearly Got Cut
The animated aurora is the most expensive thing on the page and the closest I came to deleting. It runs a fragment shader every frame, and left unmanaged it will happily run at full rate on a low-end phone with the tab in the background. The things that made it viable were all about doing less. It renders behind content that does not move, so it can run at a reduced frame rate without anyone noticing. It stops entirely when the tab is hidden, because the page visibility API exists and the default behavior of a render loop is to ignore it. It is not mounted at all on small screens, where it costs the most battery and contributes the least, since it is largely obscured by content anyway. And it respects the reduced motion preference, which is not an optimization but is the reason the whole feature is defensible. If someone has told their operating system that motion makes them unwell, a decorative shader is precisely the thing that should not render. Treat that preference as a hard requirement rather than a nice touch and the decision tree gets much simpler.
04Perceived Speed Beats Measured Speed
The change that made the site feel fastest was not an animation optimization at all. It was a thin progress bar at the top of the page during navigation. Nothing about the actual load time changed. What changed is that the gap between clicking and seeing new content stopped feeling like nothing happening and started feeling like something in progress, and that distinction is most of what people mean when they say a site feels fast. The same principle governs the reveal animations. A stagger delay long enough to look deliberate is also long enough to be irritating on the third visit, so the delays are short, in the range where they read as polish rather than as waiting. The rule I settled on is that an animation may cover a delay that already exists, but it may not create one. A reveal that plays while content is loading is free. The same reveal on content that was ready two hundred milliseconds ago is a tax the visitor pays for my aesthetics.
Takeaways
Animation on a portfolio is a budget, not a feature list. Stick to transform and opacity, stop work that nobody can see, treat reduced motion as a requirement rather than an enhancement, and remember that a progress indicator during a real wait buys more perceived speed than any easing curve.