SSR and SSG in Web Components
As front-end developers, we eventually face a common challenge: how to distribute features in a tech-agnostic way.
When systems grow — especially in large organizations — it becomes necessary to integrate features built by different teams, sometimes even from different companies. The result? A mix of technologies and frameworks coexisting in the same ecosystem.
So how do we share features effectively in such environments?
One powerful option is to use Web Components, especially when trying to stay framework-independent. And that’s where the real conversation begins.
Most modern Web Component solutions (and frameworks) couple HTML with JavaScript. Whether through template strings or JSX, this coupling often creates friction when:
- You want to render HTML on the server (SSR or SSG).
- You care about bundle size.
- You need flexibility for teams to modify the layout independently from the logic.
Wouldn’t it be great if we could separate logic and HTML entirely?
Spoiler: we can. 😉
✅ The Solution: Split HTML and Logic
By separating HTML from your JavaScript logic, you unlock some serious advantages:
- 📦 Smaller Bundles — Only JS is shipped. HTML stays out of your JavaScript.
- ✨ More Flexibility — Frontend teams can modify layout freely.
- ⚡️ SSR/SSG Friendly — Static HTML can be rendered ahead of time without simulating the app on the server.
Let’s walk through an example using a setup based on Jails, a 9kb micro-framework focused on reactive components with minimal overhead.
🔧 The Setup
I’ve created a demo repo that illustrates this concept using Parcel with TypeScript and Pug:
mfes/
├── mfe-a
├── mfe-b
└── mfe-swiper/
├── app.ts ← Logic
├── index.ts ← Entry point
├── index.css ← Styles
├── index.pug ← HTML template
└── page.pug ← Shell page
The build process outputs:
🧠 The Logic with Jails
Let’s look at how a simple Banner component (like a Swiper) looks using Jails:
export default function appSwiper({ main, on, elm, state, dependencies }) {
const wrapper = elm.querySelector('.swiper')
const { Swiper } = dependencies
const swiper = new Swiper(wrapper)
main(() => {
elm.next = next
elm.prev = prev
elm.goto = goto
elm.useSwiper = useSwiper
on('click', '[data-next]', next)
on('click', '[data-prev]', prev)
swiper.on('slideChange', onchange)
})
const onchange = () => {
state.set({ page: swiper.activeIndex + 1 })
}
const next = () => swiper.slideNext()
const prev = () => swiper.slidePrev()
const goto = (n) => swiper.slideTo(n - 1)
const useSwiper = (fn) => fn(swiper)
}
export const model = {
page: 1
}
This small function connects your component’s logic to the HTML — without bundling the HTML itself.
It exposes methods like next(), prev(), and goto(n), while keeping the actual DOM markup flexible and customizable.
Here’s how we register and start the component:
import Swiper from 'swiper'
import { register, start } from 'jails-js'
import * as appSwiper from 'components/app-swiper'
register('app-swiper', appSwiper, { Swiper })
start()
The Swiper dependency is injected externally — this means you don’t include the Swiper library in your bundle unless you need it. 💡
🧱 Using the Component
Let’s see how to use it on a page:
<app-swiper>
<div class="swiper" html-static style="height: 300px">
<div class="swiper-wrapper">
<div class="swiper-slide"><img src="https://picsum.photos/800/300?random=1" /></div>
<div class="swiper-slide"><img src="https://picsum.photos/800/300?random=2" /></div>
<div class="swiper-slide"><img src="https://picsum.photos/800/300?random=3" /></div>
<div class="swiper-slide"><img src="https://picsum.photos/800/300?random=4" /></div>
</div>
</div>
<p>Page: <strong html-inner="page">1</strong></p>
<div class="d-flex justify-content-between">
<button class="btn btn-primary" data-prev>Previous</button>
<button class="btn btn-primary" data-next>Next</button>
</div>
</app-swiper>
This gives consumers full control over the HTML structure, styling, and layout — all while maintaining access to component logic and reactive state.
🧪 Live Demo
Wanna see it in action?
Try the live version here 👉 🔗 StackBlitz Demo
✍️ Final Thoughts
Web Components are incredibly powerful — especially when designed with separation of concerns in mind.
By letting HTML live outside your logic, and exposing a clean API through JS, you empower teams to reuse features without framework lock-in, and without bloating bundles.
This strategy is particularly useful in micro-frontend environments, but works just as well in traditional apps that value performance, maintainability, and clarity.
In a future post, I’ll show how to encapsulate this component into a remote micro-frontend for full plug-and-play reuse.
Until then, feel free to explore Jails and give your components a lightweight, flexible edge 🚀