Stop Making JavaScript Do Everything - Vade Studio's Secret to Fast Web Pages

From embarrassing 30s to consistent 90s on Lighthouse - this is the story of performance optimization gone wrong, then right. What started as a typical modern web stack became a journey of unlearning, where an accidental JavaScript-free deploy revealed that sometimes less is more.

Stop Making JavaScript Do Everything - Vade Studio's Secret to Fast Web Pages

Have you ever stared at your terminal, watching yet another framework installation crawl by and wondered how you got here? You know, that moment when you realize you're installing 27,482 dependencies just to render "Hello World" on a webpage?

"This framework will solve all your problems!" they said.

"It's the future of web development!" they promised.

"Just npm install happiness-and-world-peace..." they insisted.

And then reality hits. Your beautiful, simple idea starts drowning in a sea of build configurations, render strategies and optimization techniques.

Your Lighthouse scores look like a bad grade school report card and somehow your "modern" web app takes longer to load than the entire Space Jam website did in 1996.

That's exactly where I found myself while building Vade Studio.

We have a simple yet ambitious goal - empower anyone with an idea to build modern web applications.

You know, the kind that's blazing fast, SEO friendly and plays nice with AI answer engines like Perplexity. Nothing too crazy, right?

Wrong. Dead wrong.

In an era where content discovery increasingly happens through AI-powered search and answer engines, having a slow, poorly optimized website isn't just inconvenient - it's practically invisible.

And let me tell you, watching our performance scores hover in the 30s felt like being stuck in digital purgatory.

This is the story of how we went from those embarrassing numbers to consistent 90+ performance scores. It's a tale of multiple iterations, unexpected discoveries and learning that sometimes the "modern" way isn't always the right way.

The Foundation: Understanding Vade Studio's Architecture

So how do you even begin tackling this problem? After several sleepless nights and way too much coffee, we landed on a deceptively simple idea: what if we stripped away all the framework magic and represented UI exactly as a pure data structure?

Enter our UI tree - probably the most boring-looking piece of code that ended up solving our biggest headaches:

export interface ITreeNode {
  componentName?: string       // What component to render
  sourceName?: string         // Where it came from
  hidden?: boolean            // Should we show it?
  isSourceNode?: boolean      // Is it a source component?
  id?: string                // Unique identifier
  props?: Record<string | number | symbol, any>  // Component properties
  children?: ITreeNode[]     // Child elements
}

Look at it. It's just... JSON.

No fancy decorators, no complex inheritance hierarchies, no "innovative" design patterns.

Just a simple tree structure that represents your entire UI.
Want a button? It's a node.
A layout container? Another node.
That complex data visualization? You guessed it - just another node in the tree.

But it gets interesting. By representing our entire UI as a plain JSON tree, we accidentally stumbled upon something powerful. This simple structure meant we could:

  • Serialize and deserialize the entire UI state without any framework magic
  • Transform and optimize the tree structure before rendering
  • Generate static HTML that's practically weightless
  • Keep the runtime JavaScript minimal and focused

Remember those framework promises about "just write components and we'll handle the rest"? Well, turns out sometimes handling less is actually... more.

This brings us to the real question: how do you turn this glorified JSON into blazing-fast web pages? That's where things get really interesting...

Client-Side Rendering

Remember how I mentioned our performance scores were in the 30s? Let me tell you how we got there with what seemed like a "perfectly reasonable" first implementation.

Our initial approach was straight out of the modern web development playbook. We'd fetch the UI tree from the server and let the client handle everything. The code looked clean and simple:

function renderUITree(node: ITreeNode) {
  if (!node.componentName) return null
  
  const Component = COMPONENTS[node.componentName]
  const children = node.children?.map(child => renderUITree(child))
  
  return <Component {...node.props}>{children}</Component>
}

Look at that beautiful recursive function! Three whole lines of actual logic! What could possibly go wrong?

Everything. Everything went wrong.

Here's what actually happened when someone visited a page:

  1. Browser requests page
  2. Server sends minimal HTML with a JavaScript bundle
  3. JavaScript loads and starts executing
  4. Browser fetches the UI tree
  5. JavaScript recursively walks through the tree, creating components
  6. Finally, actual content appears on screen

By the time your content showed up, you could've made a cup of coffee, checked your email and contemplated the meaning of life. Our Lighthouse scores were crying for help - hovering below 30, which in web performance terms is like showing up to a Formula 1 race in a horse-drawn carriage.

The worst part? This wasn't even a complex application. We were just rendering some static content! You know something's wrong when your static page takes longer to load than a full-blown web application.

But hey, at least we learned an important lesson:

Just because an approach is common in modern web development, it doesn't mean that it's the right one. Sometimes you need to step back and question if you're really solving the right problem.

Want to know how bad it really was? Here's what our pagespeed analysis looked like...

Performance with only client side rendering

The Server-Side Rendering + Hydration

After our client-side rendering fiasco, we did what any self-respecting team would do: we moved the rendering to the server.

But not just any server - we went full Clojure on the JVM. You know, because if you're going to fix something, you might as well flex some functional programming muscles while doing it.

The plan was beautiful in its simplicity:

(defn render-tree [node]
  (when-let [component-name (:componentName node)]
    (let [component (get-component component-name)
          children (map render-tree (:children node))]
      (component (merge (:props node) 
                       {:children children}))))

On the server side, our Clojure code would pre-render the entire UI tree into HTML.

Then, on the client side, we'd "hydrate" the page - basically telling React
"Hey, this HTML is already here, just wire up the interactivity, okay?"

The results? Our performance scores jumped from the embarrassing 30s to the mediocre 60s.

Progress! But also... not really good enough. It's like going from a F to a D+, better but nobody's putting that grade on their refrigerator.

Here's what was happening under the hood:

  1. Server pre-renders the HTML (blazing fast, thanks Clojure!)
  2. Browser receives fully formed HTML (great!)
  3. JavaScript loads (uh oh...)
  4. Hydration begins (wait for it...)
  5. React walks through the entire tree, matching components (sigh...)
  6. Finally, interactivity is restored (was it worth it?)

We'd solved the initial content display problem, but introduced a new one: the hydration tax. Our pages were showing content faster, but they were still shipping and executing more JavaScript than a crypto mining script.

The real kicker?

Most of our UI was static.

We were paying the full price of hydration for pages that barely had any interactivity. It's like buying a Ferrari to drive to your mailbox - technically it works, but there might be a more reasonable solution.

Little did we know, our journey through web performance optimization was about to take an interesting turn...

Performance with SSR and bundled javascript

The Unexpected Discovery

Like any good developer drowning in performance problems, I dove deep into the classic "maybe if I just optimize the bundle size" rabbit hole. You know the drill:

;; Before: Kitchen sink build config
{:builds 
  {:app {:target :browser
         :output-dir "public/js"
         :asset-path "/js"
         :modules {:main {:entries [app.core]}}
         :dev {...everything-but-the-kitchen-sink...}}}

;; After: Aggressively optimized
{:builds 
  {:app {:target :browser
         :output-dir "public/js"
         :asset-path "/js"
         :modules {:main {:entries [app.core]}
                  :widgets {:entries [app.widgets]}
                  :charts {:entries [app.charts]}}
         :compiler-options {:optimizations :advanced}}}}

We went full Marie Kondo on our dependencies:

  • Goodbye Malli, we don't need runtime prop validation
  • Farewell Garden, Tailwind's got us covered
  • Au revoir, every npm package we weren't 100% sure about

Hours of optimization, countless shadow-cljs configuration tweaks and enough CPU cycles burned to heat a small house in winter.

The result? We moved from the 60s to the... wait for it... 70s.

But then something magical happened. In the midst of all this JavaScript juggling, I made a mistake. A glorious, beautiful mistake. During one deploy, I somehow managed to not send any JavaScript to the client at all.

And the Lighthouse score? 91. NINETY-ONE!

I was ecstatic! "Look at these scores!" I shouted to my team, proudly showing off the blazing fast page loads. Until someone tried to click a button. And another button. And another button...

Nothing. Worked.

Back to square one?

Not quite. This accident revealed something crucial - our pages were perfectly capable of being lightning-fast. We just needed to figure out how to keep that speed while maintaining interactivity.

That's when it hit me: What if we only sent the JavaScript that actually enables interactivity? What if our server-rendered system could handle this natively, without any additional complexity?

It was time to stop fighting with bundle optimizations and start thinking about a completely different approach...

The Perfect Solution

So there I was, deep in the trenches of JavaScript optimization hell, looking for answers. First stop: htmx. Have you ever looked at its implementation? It's like someone took jQuery, sprinkled in some modern web features and called it a day. Not exactly what I was looking for.

Then came the "maybe I'm just not trying hard enough" phase. I started doing things that would make any sensible developer question my sanity:

// Desperate attempt #1: The "setTimeout hack"
setTimeout(() => loadAllTheJavaScript(), 1000) // Sorry, user, your buttons will work... eventually

// Desperate attempt #2: Let's throw it in a web worker!
import { partytownSnippet } from '@builder.io/partytown/integration'
// It did not work with hydration

After banging my head against the wall for what felt like eternity, I did what we all should do more often:

I stepped away. Spent time with my kids. Got some sleep. Did the school run.

And then, over my morning coffee, it hit me. Alpine.js. That elegant little library I'd used years ago and somehow forgotten about. You know that moment when you remember an old friend and wonder "how did I forget about you?"

The solution was suddenly crystal clear: Instead of fighting with hydration and complex client-side frameworks, what if we just compiled our event handling into Alpine.js directives?

From this:

{:componentName "Button"
 :props {:onClick {:type "function"
                  :body "handleClick(event)"}}}

To this:

<button x-on:click="handleClick($event)">
  Click me!
</button>

No hydration. No complex bundling. No setTimeout hacks. Just clean, progressive enhancement that works.

When we deployed this solution, our performance score hit 92.

NINETY-TWO!

And this time, everything actually worked!

Sometimes the best solutions aren't found in the latest frameworks or clever hacks. Sometimes they're found in that quiet moment over coffee, when you remember there's beauty in simplicity.

The Path Forward: Less JavaScript, More Coffee

So what did we learn from this rollercoaster of web performance optimization?

First, the obvious: modern web development is broken.

We've somehow convinced ourselves that throwing more JavaScript at performance problems will somehow make them go away.

It's like trying to make your car go faster by adding more weight - technically you're doing something, but it's probably not helping.

But the real lessons went deeper:

  1. Sometimes the best solutions come from constraints, not capabilities. Every time we removed something - client-side rendering, hydration, heavy runtime libraries - our application got better. It turns out the fastest JavaScript is often the JavaScript you don't ship.
  2. The path to high performance isn't always about adding the newest, shiniest tools. Sometimes it's about rediscovering simpler solutions that have been there all along. Alpine.js wasn't new or revolutionary - it was just right for the job.
  3. Most importantly, we learned that stepping away from the problem can be as important as diving into it. That "aha!" moment didn't come during a late-night coding session or while reading the latest framework documentation. It came over a cup of coffee, after a good night's sleep and a school run.

Today, Vade Studio renders static pages with performance scores in the 90s, proving that you don't have to choose between modern web applications and blazing fast performance. You can have both.

We're just getting started on this journey of making web development simpler, faster and more accessible. As we continue building the future of app development with AI and no-code, we're discovering new ways to push the boundaries of what's possible.

Want to stay updated on our progress? Subscribe to our newsletter below. We'll share our latest discoveries, technical deep-dives and insights as we continue building the future of web development. No framework fatigue, just practical solutions for real problems.

After all, the web should be fast. Building for it should be fun. And maybe, just maybe, we can help make both of those things true.

Subscribe to Vade Bytes

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe