Unlocking Performance and Flexibility: Exploring Hybrid Rendering with Micro front-end.

THOMAS RUMAS
ADEO Tech Blog
Published in
12 min readNov 9, 2023

In the ever-evolving landscape of web development, delivering a seamless user experience while optimizing performance is a constant challenge. A promising approach to address these challenges is “Hybrid Rendering,” a technique that marries the best of server-side and client-side rendering methodologies.

What is Hybrid Rendering?

Hybrid Rendering is a web development technique that combines the power of server-side rendering (SSR) and client-side rendering (CSR) to strike a delicate balance between performance and interactivity. In essence, it allows web developers to share the same templating system between the server and client sides of their applications.

Traditionally, web applications have primarily employed either SSR or CSR. SSR, while delivering fast initial page loads and Search Engine Optimization (SEO) benefits, can sometimes lead to slower interactivity due to frequent full-page refreshes. On the other hand, CSR offers a more responsive and interactive user experience, but often results in slower initial page loads and SEO challenges.

Hybrid Rendering seeks to transcend these limitations by blending the strengths of both approaches. It enables developers to render the initial page on the server-side, delivering a fast and fully formed HTML document to the client, ensuring a speedy first contentful paint. Subsequently, it allows client-side JavaScript to take over and hydrate the page, enabling dynamic interactions without the need for full-page reloads.

Why is it Interesting to share the same templating system between server and client Side?

Sharing the same templating system between the server and client sides of a web project brings forth several compelling advantages:

Consistency: By utilizing a unified templating system, developers can maintain consistency in the user interface (UI) and user experience across the application. This consistency reduces the likelihood of rendering discrepancies and enhances the overall polish of the application.

Code Reusability: A shared templating system encourages code reuse. Templates defined on the server can often be reused on the client side, reducing duplication and promoting a more maintainable codebase.

Improved SEO: Search engine optimization (SEO) is crucial for the discoverability of web content. Hybrid Rendering ensures that search engine crawlers receive fully-rendered HTML content, enhancing SEO performance compared to CSR-only approaches.

Performance Optimization: Hybrid Rendering minimizes the time required for the initial page load by delivering a fully rendered page from the server. This reduces the perceived load time and contributes to a snappier user experience.

Progressive Enhancement: It allows for progressive enhancement, where the core functionality is available immediately, and more interactive features are added as the client-side JavaScript hydrates the page. This provides a graceful degradation path for users with slower devices or network connections.

Image explaining hybrid rendering
Picture explaining Hybrid Rendering from NuxtJS official documentation.

To be more competitive inside the job market.

In today’s competitive tech job market, finding and retaining top-tier talent is an ongoing challenge for businesses of all sizes. When it comes to recruiting new developers, the subject matter you choose to showcase can significantly impact your ability to attract skilled professionals who are not only proficient but also enthusiastic about the work.

Hybrid Rendering is a topic that holds tremendous appeal for prospective developers. Here’s why:

Cutting-Edge Technology: New developers are often eager to work on projects that incorporate the latest technologies and best practices. Hybrid Rendering represents a forward-thinking approach to web development, and it’s an excellent way to demonstrate that your organization is at the forefront of the industry.

Balanced Skill Set: Hybrid Rendering requires a diverse skill set that encompasses both server-side and client-side development. This means developers working on such projects can gain valuable experience in a wide range of technologies, from server-side rendering frameworks to front-end JavaScript libraries. This balance can be particularly enticing for developers who want to round out their skill set.

Impactful User Experience: Developers are often driven by the opportunity to create meaningful and impactful user experiences. Hybrid Rendering, with its ability to deliver fast initial page loads and highly interactive applications, aligns with this aspiration. Knowing that their work directly contributes to a smoother, more engaging user experience can be a strong motivator for new recruits.

Innovation and Problem Solving: Developing Hybrid Rendering solutions often involves solving complex technical challenges. New developers who relish problem-solving and innovation may be drawn to these opportunities, knowing they will be tasked with finding creative solutions to improve web application performance and interactivity.

Collaborative Environment: Implementing Hybrid Rendering typically involves collaboration among developers, designers, and SEO experts. This collaborative environment fosters a sense of teamwork and allows new developers to learn from professionals with different specializations.

Trying to use this pattern with micro front-end

For a deeper understanding of how micro front-end architecture can revolutionize the way you build and scale web applications, be sure to check out our comprehensive article on ‘Behind leroymerlin.fr: Micro Frontends’.

How does the micro front-end work in ADEO?

schema explaining our micro front-end solution

Every request sent by the user’s browser is caught by our orchestrator of the micro front-end, and transferred to every micro front-end with context information like language, logged… The micro front-end will send an HTML response based on Kobi’s transferred request.

Our orchestrator will now concatenate every micro front-end response and send it to the browser. We use the power of CDN to deliver static files like CSS and JS.

Define objectives

First, we need to declare the objective and based on which KPI we can validate the new stack:

  • As the same Web Performance that our historical JAVA stack
  • Be compliant with our micro front-end contract
  • Be satisfying for developer experience

Testing actual market solutions

We’ve tried severals solutions from JavaScript ecosystem like:

  • Nuxt based on Vue.js 3
  • Svelte as template engine
  • Sveltekit…

Nuxt

We decided to use Vue.js to deliver Single Page Application when needed on our Website based on our architecture due to its ability to be easier to learn and develop, and also performant, so by evidence, NuxtJS was the first framework we tried.

With NuxtJS we found some lack of Web performance when we needed to perform a huge amount of requests, after monitoring our first POC, we saw that the bottleneck is when we needed to compile HTML.

JavaScript is a language that works on only thread, we don’t have the power of more robust solutions like C++ or JAVA to perform on multi-threading, compiling HTML is the most expensive part in terms of server processing time.

Also, when checking on client-side we weren’t satisfied by sending a framework like Vue.js to perform hydration. Sending a runtime that weighs 20Kb is too huge, inside our Micro Front-End context, we can have a plural micro front-end and another paint point was “How can we scope Vue.js runtime?”. Even if we are using a micro front-end architecture, when it comes to be delivered on the client’s browser, we have only one HTML document and we cannot scoped JavaScript to be executed, how the browser will perform if we send two versions or more of Vue.js runtime?

We cannot take this risk, we need to be sure, to deliver the best User Experience, how our micro front-ends will collaborate inside the client’s browser.

Svelte

And we heard about Svelte, a new library, lightweight, which is performant for DOM Manipulation with its “reactivity” system, easy to learn like Vue.js…

First of all, we tried to use it as a template engine, and it works! Coupled with NestJS, a NodeJS framework to design Web projects, we were able to deliver a solution that works, suitable with our micro front-end architecture, compiling faster HTML… But in fact, we’ve just found an other solution to perform Server Side Rendering, we weren’t able to:

  • Use Single File Component pattern
  • Perform hybrid rendering

And we heard about SvelteKit, a solution that can perform and manage Hybrid rendering! But after trying it, and it’s logical, Sveltekit is designed to manage a complete website, and that’s not our case, we needed to be compliant with micro front-end, and therefore breaking or reverse engineering the solution… it will not be suitable for long term support.

If we create our own solution?

Rich from our previous POC, we had the conviction that any actual market solution will be suitable for our micro front-end contract, so let’s try it!

We decided to use this two technologies:

  • NestJS as a “back-end” framework to handle micro front-end requests
  • Svelte to handle “View” and JavaScript client side

To declare this new solution as a success based on our criterias we wanted also:

  • Single file component usage from Svelte
  • Autonomous of our micro front-end, no runtime shared
  • Be compliant with our technical tools like Vault, Github Actions…
  • Handle i18n without any client side solution
  • And be performant!

The first topic was, how will we be able to use the same Svelte files for Server Side and Client Side? Rich from our Sveltekit experience, we thought and an idea came, “What if we are able to perform a double compilation of Svelte files?”

Double compilation

To perform a double compilation of Svelte files, this is how we do inside Rollup configuration:

...
const clientPlugins = () => [
svelte({
{
compilerOptions: {
customElement: false,
css: false,
hydratable: true,
},
},
}),
css({ output: false }),
];

const viewPlugins = (prmName) => [
svelte({
{
compilerOptions: {
css: true,
generate: 'ssr',
hydratable: true,
},
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: `${prmName}.css` }),
resolve({
browser: true,
dedupe: ['svelte'],
}),
mv({
src: `views/${prmName}.css`,
dest: `public/${prmName}.css`,
overwrite: true,
}),
];

With this configuration we are able to:

  • Extract SCSS/CSS from Svelte files when we are compiling for Server Side Rendering usage
  • Allow Svelte to perform hydration from Server Side rendered HTML

Declare our “template engine” to NestJS

async function bootstrapApp() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: winstonLogger(),
});

app.useStaticAssets(join(__dirname, '../..', 'public'), {
prefix: '/static',
});
app.engine('js', svelteTemplateEngine);
app.setViewEngine('js');

await app.listen(3000);
Logger.log(`Server listening: ${await app.getUrl()}`);
}

If you have a look at this code, you can see that we aren’t using Svelte files as template engine but our compiled one in plain JS files. We use a custom function to compile HTML.

export async function svelteTemplateEngine(
filePath: string,
options: any,
next,
) {
const data = options;
delete data.settings;

if (process.env.NODE_ENV !== 'production') {
delete require.cache[filePath];
}

// eslint-disable-next-line @typescript-eslint/no-var-requires
const Component = require(filePath);
const { html } = Component.render(data);
let htmlResponse = `<div id="${data.ssr}">${html}</div>`;
if (data.hydration) {
const model = data.dataForHydration ? data.dataForHydration : data;
htmlResponse += `<script>window.${toSnakeCase(data.ssr)} = ${JSON.stringify(
model,
)}</script>`;
}
if (process.env.NODE_ENV === 'local') {
htmlResponse += injectAssetsOnDevMode(filePath);
}
next(null, htmlResponse);
}

In this function, we will call the render method of our compiled JS files and sent an HTML response, as you can see, if we need to perform hydration we will send the context used inside rendered HTML on Server Side Rendering, it’s needed, if we don’t send it, we will have an hydration error and it’s logical:

“In web development, hydration or rehydration is a technique in which client-side JavaScript converts a static HTML web page, delivered either through static hosting or server-side rendering, into a dynamic web page by attaching event handlers to the HTML elements.” — Wikipedia

So to be able to transform the HTML rendered to a dynamic one, we need to have the context used to be sure that we have the same one on Server Side and Client Side.

Handle a request

And, because we are based on NestJS framework to handle “back-end” of our micro front-end, if you are aware of the usage of NestJS, you will handle it as you already know by your controller, we will just add some context to our views:

@Get('/my-route')
@Render('Main')
getFragment(@Headers() headers): Observable<ReturnTypeWithMandatoryKeys> {
return zip(
this.appService.getNbaPlayers(),
this.appService.getNbaGames(),
).pipe(
map((playersAndTeams) => {
return {
hydration: true,
dataForHydration: { nbaPlayers: playersAndTeams[0].data },
nbaPlayers: playersAndTeams[0].data,
nbaGames: playersAndTeams[1].data,
message: this.appService.getMessage(),
meta: playersAndTeams[0].meta,
// ssr is mandatory to match correctly your HTML output
ssr: 'my-fragment',
};
}),
);
}

By default, we recommended partial hydration!

“Partial hydration is the same process as hydration but even performing a full hydration on our complete DOM, we will perform it on a specific part.” — Wikipedia

We will explain it with a “business” example:

  • My fragment rendered on Server Side, a complete HTML document with a filter system and a list of products
  • I need only to create DOM interaction on my filter because my list of product will be rerendered with an XHR request based on user’s filter
  • I will only pass the needed data to perform hydration on my filter and specify hydration only on filter’s DOM

You will improve web page performance: as JavaScript DOM manipulations are synchronous, by reducing the browser’s need to constantly check for dynamic rendering or event attachment to DOM elements, you can save time and decrease the total blocking time metrics, achieving faster interactivity.

Handle JS on client side

And now, how can we handle JavaScript on the client side? And it’s simple, we will declare our “Svelte App” and init it when needed with Vanilla JS.

import NbaGames from '../views/components/NbaGames.svelte';
import NbaPlayers from '../views/components/NbaPlayers.svelte';

document.addEventListener('DOMContentLoaded', () => {
new HandleHydration();
new HandleNbaGames();
});

class HandleHydration {
constructor() {
new NbaPlayers({
target: document.getElementById('hydrated-client-rendering-players'),
hydrate: true,
props: window.my_fragment,
});
}
}

//Vanilla JS way to rerender a list of NBA Games without hydration
class HandleNbaGames {
#domElement;
#trigger;
constructor() {
this.#domElement = document.getElementById('client-rendering-games');
this.initEventOnTrigger();
}
initEventOnTrigger() {
this.#trigger = document.getElementById('triggerNbaGames');
this.#trigger.addEventListener('click', async () => {
const nbaGames = await this.getNbaGames();
this.#domElement.innerHTML = '';
new NbaGames({
target: this.#domElement,
props: {
nbaGames: nbaGames,
},
});
});
}
async getNbaGames() {
const response = await fetch('/api/games?page=2');
const { data } = await response.json();
return data;
}
}

If you know how to declare a Svelte App on client side, you can see that’s it’s the same process, we call the constructor of our Svelte component, and give him the needed properties:

  • The DOM element that contains our rendered HTML
  • Hydrate boolean to tells Svelte engine that we will perform on rendered HTML
  • Props needed to use, passed inside rendered HTML

And that’s it, you don’t need to do anything more!

And for internationalization?

Like we saw previously, we will use the power of Server Side Rendering to handle i18n! In most cases, you only need a dictionary with keys and values for languages.

As we saw, our Kobi orchestrator is able to pass the user’s context on every request. So what we do is:

  • Retrieve dictionaries on CI/CD and bundled them inside micro front-end
  • Get languages from user’s context at every request
  • Send needed keys to the HTML response

So 99% of time, it’s enough for our cases, but sometimes you can have a multi-language website and your users can change their preferred languages on the client side, how can you handle it?

Simply with an XHR request to your back-end, you will pass the new language request and send a JSON response with the new dictionary, transfer it to your Svelte component on client side and it will render your HTML with the new language!

Are we Web Performant?

To answer this question, we will perform a synthetic test with Speedcurve (a tool to monitor web performance) based on Lighthouse and Google Web Vitals recommendation.

This test will be performed on mobile and with a 4G connection with a download bandwidth of 11.7 Mbps.

And here it’s the result:

Result of Web Performance test

If we compare with Google Web Vitals recommendation:

  • The First Contentful Paint is very good with an apparition of 900ms
  • The Largest element is shown before 2.5s
  • The page is visually complete under 4s

So regarding this result and also our continued monitoring with Synthetic and Real User Monitoring, at this time, the answer is yes! But we need to be vigilant! Due to the complexity of hybrid rendering, you can easily decrease your web performance indicator on Server Side or Client Side!

You need to be vigilant on:

  • Do I really need to perform hydration, even if it’s a partial one? Can I do it with Vanilla JS?
  • Am I sure that I’m sending the minimal amount of needed properties to perform hydration to avoid weighing down my HTML response?
  • Do my client side parts not render too much time?

Source: Classement webperf mobile : Leroy Merlin en tête dans l’e-commerce devant Showroom privé

Conclusion

If you are driven by Web performance, every technology can be used! You just need to be vigilant and use them for the good use case.

By choosing to create our own solution we are able to be more suitable with our internal business use cases and solutions but it’s not developer friendly as major frameworks on the JavaScript market.

You will need to train your developers to handle the complexity of your solution, a misunderstanding can occur faster than you think, and the regression isn’t far!

Even if we have this solution available now inside your technological choice, our first recommendation is to use a Server Side Rendering solution to be performant, and we aren’t alone with this conviction.

If you had a look recently on Angular, React… you can see that they are currently working or have delivered a solution to perform Server Side Rendering to be more performant!

Sources:

--

--

THOMAS RUMAS
ADEO Tech Blog

Software Engineer for Leroy Merlin France. I’m in love with Front-End development