Vue.js Server-Side Rendering (SSR)

Server-Side Rendering (SSR) in Vue.js is a powerful technique used to render your Vue.js applications on the server rather than directly in the browser. This approach can significantly improve performance, SEO (Search Engine Optimization), and overall user experience by delivering fully rendered HTML to the client.

What is Server-Side Rendering (SSR)?

Server-Side Rendering (SSR) is the process of rendering web pages on the server and delivering the fully rendered HTML to the browser. In contrast, Client-Side Rendering (CSR), which is typical for Single Page Applications (SPAs), involves the browser downloading JavaScript and rendering the page dynamically.

With SSR, the HTML is pre-rendered on the server, meaning the user can see the content almost immediately, while CSR requires the JavaScript to be fully loaded before the content is visible.

Key Differences Between CSR and SSR:

  • CSR (Client-Side Rendering):
    • JavaScript runs in the browser, and the content is rendered dynamically.
    • The initial page load may be slow due to the time it takes to load JavaScript.
    • Better for user interactions after the page is loaded.
  • SSR (Server-Side Rendering):
    • HTML is pre-rendered on the server, so content appears faster.
    • Search engines can easily index the content.
    • Initial load is faster, but interactions after the load may be slightly slower.

Why Use Server-Side Rendering?

SSR has several benefits compared to client-side rendering, especially for SEO and performance:

  1. Improved SEO: Search engines rely on the HTML content of the page to index your website. With client-side rendering, the content may not be available immediately, which can hurt your SEO. SSR ensures that the content is fully available when the page is loaded, making it easier for search engines to crawl and index.

  2. Faster Time-to-Content (TTC): With SSR, the initial load time is significantly reduced since the content is rendered on the server. This improves user experience, especially for users with slower connections.

  3. Better Performance on Slow Devices: Devices with lower computational power, such as mobile phones, can struggle with client-side rendering. SSR shifts the load to the server, ensuring a better experience for users with slower devices.

  4. Social Media Previews: When sharing pages on social media, platforms often scrape the HTML for metadata and content. SSR ensures that this content is available and displayed correctly when shared.

Vue.js provides built-in support for SSR through a package called vue-server-renderer. The basic idea is that instead of rendering the application directly in the browser, you render it on the server and send the fully rendered HTML to the client.

The process typically involves the following steps:

  1. Server Setup: The Vue.js application is set up to run on a Node.js server.
  2. Vue Component Rendering: The Vue components are rendered to HTML on the server.
  3. Client Receives Pre-rendered HTML: The browser receives the rendered HTML and displays it.
  4. Hydration: The client-side Vue instance “hydrates” the page, meaning it attaches to the already rendered HTML and makes it interactive.

Setting Up a Simple Vue.js SSR Application

Let’s walk through creating a basic Vue.js SSR application using Node.js.

Step 1: Install Dependencies

You will need to install Vue and the vue-server-renderer package for SSR. Also, you’ll need to install Express.js, a web server for Node.js.

				
					npm install vue vue-server-renderer express
				
			

Step 2: Create Vue Components

Create a simple Vue component in App.vue:

				
					// src/App.vue
<template>
  <div id="app">
    <h1>{{ message }}</h1>
  </div>
</template> <script type="litespeed/javascript">export default{data(){return{message:'Hello, Vue.js SSR!'}}}</script> 
				
			

Step 3: Server Setup for SSR

Create an Express server to handle SSR. This server will render the Vue component to a string and send it to the client.

				
					// server.js
const express = require('express');
const { createRenderer } = require('vue-server-renderer');
const Vue = require('vue');

// Create an Express server
const app = express();

// Create a renderer
const renderer = createRenderer({
  template: `
    <html>
      <head>
        <title>Vue.js SSR</title>
      </head>
      <body>
         <script data-no-optimize="1">!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).LazyLoad=e()}(this,function(){"use strict";function e(){return(e=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,a=arguments[e];for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(t[n]=a[n])}return t}).apply(this,arguments)}function i(t){return e({},it,t)}function o(t,e){var n,a="LazyLoad::Initialized",i=new t(e);try{n=new CustomEvent(a,{detail:{instance:i}})}catch(t){(n=document.createEvent("CustomEvent")).initCustomEvent(a,!1,!1,{instance:i})}window.dispatchEvent(n)}function l(t,e){return t.getAttribute(gt+e)}function c(t){return l(t,bt)}function s(t,e){return function(t,e,n){e=gt+e;null!==n?t.setAttribute(e,n):t.removeAttribute(e)}(t,bt,e)}function r(t){return s(t,null),0}function u(t){return null===c(t)}function d(t){return c(t)===vt}function f(t,e,n,a){t&&(void 0===a?void 0===n?t(e):t(e,n):t(e,n,a))}function _(t,e){nt?t.classList.add(e):t.className+=(t.className?" ":"")+e}function v(t,e){nt?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\s+)"+e+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")}function g(t){return t.llTempImage}function b(t,e){!e||(e=e._observer)&&e.unobserve(t)}function p(t,e){t&&(t.loadingCount+=e)}function h(t,e){t&&(t.toLoadCount=e)}function n(t){for(var e,n=[],a=0;e=t.children[a];a+=1)"SOURCE"===e.tagName&&n.push(e);return n}function m(t,e){(t=t.parentNode)&&"PICTURE"===t.tagName&&n(t).forEach(e)}function a(t,e){n(t).forEach(e)}function E(t){return!!t[st]}function I(t){return t[st]}function y(t){return delete t[st]}function A(e,t){var n;E(e)||(n={},t.forEach(function(t){n[t]=e.getAttribute(t)}),e[st]=n)}function k(a,t){var i;E(a)&&(i=I(a),t.forEach(function(t){var e,n;e=a,(t=i[n=t])?e.setAttribute(n,t):e.removeAttribute(n)}))}function L(t,e,n){_(t,e.class_loading),s(t,ut),n&&(p(n,1),f(e.callback_loading,t,n))}function w(t,e,n){n&&t.setAttribute(e,n)}function x(t,e){w(t,ct,l(t,e.data_sizes)),w(t,rt,l(t,e.data_srcset)),w(t,ot,l(t,e.data_src))}function O(t,e,n){var a=l(t,e.data_bg_multi),i=l(t,e.data_bg_multi_hidpi);(a=at&&i?i:a)&&(t.style.backgroundImage=a,n=n,_(t=t,(e=e).class_applied),s(t,ft),n&&(e.unobserve_completed&&b(t,e),f(e.callback_applied,t,n)))}function N(t,e){!e||0<e.loadingCount||0<e.toLoadCount||f(t.callback_finish,e)}function C(t,e,n){t.addEventListener(e,n),t.llEvLisnrs[e]=n}function M(t){return!!t.llEvLisnrs}function z(t){if(M(t)){var e,n,a=t.llEvLisnrs;for(e in a){var i=a[e];n=e,i=i,t.removeEventListener(n,i)}delete t.llEvLisnrs}}function R(t,e,n){var a;delete t.llTempImage,p(n,-1),(a=n)&&--a.toLoadCount,v(t,e.class_loading),e.unobserve_completed&&b(t,n)}function T(o,r,c){var l=g(o)||o;M(l)||function(t,e,n){M(t)||(t.llEvLisnrs={});var a="VIDEO"===t.tagName?"loadeddata":"load";C(t,a,e),C(t,"error",n)}(l,function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_loaded),s(e,dt),f(n.callback_loaded,e,a),i||N(n,a),z(l)},function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_error),s(e,_t),f(n.callback_error,e,a),i||N(n,a),z(l)})}function G(t,e,n){var a,i,o,r,c;t.llTempImage=document.createElement("IMG"),T(t,e,n),E(c=t)||(c[st]={backgroundImage:c.style.backgroundImage}),o=n,r=l(a=t,(i=e).data_bg),c=l(a,i.data_bg_hidpi),(r=at&&c?c:r)&&(a.style.backgroundImage='url("'.concat(r,'")'),g(a).setAttribute(ot,r),L(a,i,o)),O(t,e,n)}function D(t,e,n){var a;T(t,e,n),a=e,e=n,(t=It[(n=t).tagName])&&(t(n,a),L(n,a,e))}function V(t,e,n){var a;a=t,(-1<yt.indexOf(a.tagName)?D:G)(t,e,n)}function F(t,e,n){var a;t.setAttribute("loading","lazy"),T(t,e,n),a=e,(e=It[(n=t).tagName])&&e(n,a),s(t,vt)}function j(t){t.removeAttribute(ot),t.removeAttribute(rt),t.removeAttribute(ct)}function P(t){m(t,function(t){k(t,Et)}),k(t,Et)}function S(t){var e;(e=At[t.tagName])?e(t):E(e=t)&&(t=I(e),e.style.backgroundImage=t.backgroundImage)}function U(t,e){var n;S(t),n=e,u(e=t)||d(e)||(v(e,n.class_entered),v(e,n.class_exited),v(e,n.class_applied),v(e,n.class_loading),v(e,n.class_loaded),v(e,n.class_error)),r(t),y(t)}function $(t,e,n,a){var i;n.cancel_on_exit&&(c(t)!==ut||"IMG"===t.tagName&&(z(t),m(i=t,function(t){j(t)}),j(i),P(t),v(t,n.class_loading),p(a,-1),r(t),f(n.callback_cancel,t,e,a)))}function q(t,e,n,a){var i,o,r=(o=t,0<=pt.indexOf(c(o)));s(t,"entered"),_(t,n.class_entered),v(t,n.class_exited),i=t,o=a,n.unobserve_entered&&b(i,o),f(n.callback_enter,t,e,a),r||V(t,n,a)}function H(t){return t.use_native&&"loading"in HTMLImageElement.prototype}function B(t,i,o){t.forEach(function(t){return(a=t).isIntersecting||0<a.intersectionRatio?q(t.target,t,i,o):(e=t.target,n=t,a=i,t=o,void(u(e)||(_(e,a.class_exited),$(e,n,a,t),f(a.callback_exit,e,n,t))));var e,n,a})}function J(e,n){var t;et&&!H(e)&&(n._observer=new IntersectionObserver(function(t){B(t,e,n)},{root:(t=e).container===document?null:t.container,rootMargin:t.thresholds||t.threshold+"px"}))}function K(t){return Array.prototype.slice.call(t)}function Q(t){return t.container.querySelectorAll(t.elements_selector)}function W(t){return c(t)===_t}function X(t,e){return e=t||Q(e),K(e).filter(u)}function Y(e,t){var n;(n=Q(e),K(n).filter(W)).forEach(function(t){v(t,e.class_error),r(t)}),t.update()}function t(t,e){var n,a,t=i(t);this._settings=t,this.loadingCount=0,J(t,this),n=t,a=this,Z&&window.addEventListener("online",function(){Y(n,a)}),this.update(e)}var Z="undefined"!=typeof window,tt=Z&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),et=Z&&"IntersectionObserver"in window,nt=Z&&"classList"in document.createElement("p"),at=Z&&1<window.devicePixelRatio,it={elements_selector:".lazy",container:tt||Z?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_poster:"poster",class_applied:"applied",class_loading:"litespeed-loading",class_loaded:"litespeed-loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1},ot="src",rt="srcset",ct="sizes",lt="poster",st="llOriginalAttrs",ut="loading",dt="loaded",ft="applied",_t="error",vt="native",gt="data-",bt="ll-status",pt=[ut,dt,ft,_t],ht=[ot],mt=[ot,lt],Et=[ot,rt,ct],It={IMG:function(t,e){m(t,function(t){A(t,Et),x(t,e)}),A(t,Et),x(t,e)},IFRAME:function(t,e){A(t,ht),w(t,ot,l(t,e.data_src))},VIDEO:function(t,e){a(t,function(t){A(t,ht),w(t,ot,l(t,e.data_src))}),A(t,mt),w(t,lt,l(t,e.data_poster)),w(t,ot,l(t,e.data_src)),t.load()}},yt=["IMG","IFRAME","VIDEO"],At={IMG:P,IFRAME:function(t){k(t,ht)},VIDEO:function(t){a(t,function(t){k(t,ht)}),k(t,mt),t.load()}},kt=["IMG","IFRAME","VIDEO"];return t.prototype={update:function(t){var e,n,a,i=this._settings,o=X(t,i);{if(h(this,o.length),!tt&&et)return H(i)?(e=i,n=this,o.forEach(function(t){-1!==kt.indexOf(t.tagName)&&F(t,e,n)}),void h(n,0)):(t=this._observer,i=o,t.disconnect(),a=t,void i.forEach(function(t){a.observe(t)}));this.loadAll(o)}},destroy:function(){this._observer&&this._observer.disconnect(),Q(this._settings).forEach(function(t){y(t)}),delete this._observer,delete this._settings,delete this.loadingCount,delete this.toLoadCount},loadAll:function(t){var e=this,n=this._settings;X(t,n).forEach(function(t){b(t,e),V(t,n,e)})},restoreAll:function(){var e=this._settings;Q(e).forEach(function(t){U(t,e)})}},t.load=function(t,e){e=i(e);V(t,e)},t.resetStatus=function(t){r(t)},Z&&function(t,e){if(e)if(e.length)for(var n,a=0;n=e[a];a+=1)o(t,n);else o(t,e)}(t,window.lazyLoadOptions),t});!function(e,t){"use strict";function a(){t.body.classList.add("litespeed_lazyloaded")}function n(){console.log("[LiteSpeed] Start Lazy Load Images"),d=new LazyLoad({elements_selector:"[data-lazyloaded]",callback_finish:a}),o=function(){d.update()},e.MutationObserver&&new MutationObserver(o).observe(t.documentElement,{childList:!0,subtree:!0,attributes:!0})}var d,o;e.addEventListener?e.addEventListener("load",n,!1):e.attachEvent("onload",n)}(window,document);</script><script data-no-optimize="1">var litespeed_vary=document.cookie.replace(/(?:(?:^|.*;\s*)_lscache_vary\s*\=\s*([^;]*).*$)|^.*$/,"");litespeed_vary||fetch("/wp-content/plugins/litespeed-cache/guest.vary.php",{method:"POST",cache:"no-cache",redirect:"follow"}).then(e=>e.json()).then(e=>{console.log(e),e.hasOwnProperty("reload")&&"yes"==e.reload&&(sessionStorage.setItem("litespeed_docref",document.referrer),window.location.reload(!0))});</script><script data-optimized="1" type="litespeed/javascript" data-src="https://diginode.in/wp-content/litespeed/js/96d0e6d4ba93134cbdab615e06eb2824.js?ver=a1a89"></script><script>const litespeed_ui_events=["mouseover","click","keydown","wheel","touchmove","touchstart"];var urlCreator=window.URL||window.webkitURL;function litespeed_load_delayed_js_force(){console.log("[LiteSpeed] Start Load JS Delayed"),litespeed_ui_events.forEach(e=>{window.removeEventListener(e,litespeed_load_delayed_js_force,{passive:!0})}),document.querySelectorAll("iframe[data-litespeed-src]").forEach(e=>{e.setAttribute("src",e.getAttribute("data-litespeed-src"))}),"loading"==document.readyState?window.addEventListener("DOMContentLoaded",litespeed_load_delayed_js):litespeed_load_delayed_js()}litespeed_ui_events.forEach(e=>{window.addEventListener(e,litespeed_load_delayed_js_force,{passive:!0})});async function litespeed_load_delayed_js(){let t=[];for(var d in document.querySelectorAll('script[type="litespeed/javascript"]').forEach(e=>{t.push(e)}),t)await new Promise(e=>litespeed_load_one(t[d],e));document.dispatchEvent(new Event("DOMContentLiteSpeedLoaded")),window.dispatchEvent(new Event("DOMContentLiteSpeedLoaded"))}function litespeed_load_one(t,e){console.log("[LiteSpeed] Load ",t);var d=document.createElement("script");d.addEventListener("load",e),d.addEventListener("error",e),t.getAttributeNames().forEach(e=>{"type"!=e&&d.setAttribute("data-src"==e?"src":e,t.getAttribute(e))});let a=!(d.type="text/javascript");!d.src&&t.textContent&&(d.src=litespeed_inline2src(t.textContent),a=!0),t.after(d),t.remove(),a&&e()}function litespeed_inline2src(t){try{var d=urlCreator.createObjectURL(new Blob([t.replace(/^(?:<!--)?(.*?)(?:-->)?$/gm,"$1")],{type:"text/javascript"}))}catch(e){d="data:text/javascript;base64,"+btoa(t.replace(/^(?:<!--)?(.*?)(?:-->)?$/gm,"$1"))}return d}</script></body>
    </html>
  `
});

// Handle incoming requests
app.get('*', (req, res) => {
  // Create a Vue instance
  const app = new Vue({
    data: {
      message: 'Hello from Vue.js SSR!'
    },
    template: `<div>{{ message }}</div>`
  });

  // Render the Vue instance to HTML
  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error');
      return;
    }
    res.end(html);
  });
});

// Start the server
app.listen(8080, () => {
  console.log('Server running at http://localhost:8080');
});

				
			

Step 4: Run the Server

Run the Node.js server:

				
					node server.js
				
			

Open your browser and navigate to http://localhost:8080. You should see the message “Hello from Vue.js SSR!” displayed on the page.

Hydration in Vue.js SSR

After the server renders the HTML and sends it to the client, Vue.js will take over on the client side to make the page interactive. This process is called hydration. Hydration allows Vue.js to attach the client-side Vue instance to the already-rendered HTML without re-rendering it from scratch.

To enable hydration, you can add a simple <script> tag to include Vue.js on the client side:

				
					<script type="litespeed/javascript" data-src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script type="litespeed/javascript">new Vue({el:'#app',data:{message:'Hello from Vue.js SSR!'}})</script> 
				
			

With this, Vue.js will “hydrate” the static HTML and make it interactive.

Advanced Concepts in Vue.js SSR

Handling Asynchronous Data

One of the most powerful features of SSR in Vue.js is handling asynchronous data, such as fetching data from an API before rendering the page.

To handle asynchronous data in SSR, you can use the asyncData hook (if you’re using Nuxt.js) or manually handle promises in your server code.

				
					const app = new Vue({
  data: {
    message: ''
  },
  template: `<div>{{ message }}</div>`,
  created() {
    // Simulate an asynchronous API call
    setTimeout(() => {
      this.message = 'Data fetched from API!';
    }, 1000);
  }
});

				
			

When rendering the Vue app on the server, you’ll need to wait for the asynchronous data to resolve before sending the HTML to the client.

Caching in SSR

Rendering Vue components on the server can be resource-intensive. To optimize performance, you can implement caching mechanisms to cache frequently accessed pages. A common approach is to use lru-cache to store HTML strings and serve them when needed.

				
					npm install lru-cache

				
			
				
					const LRU = require('lru-cache');
const cache = new LRU({ max: 100 });

// Render function with caching
function renderToCache(app, res) {
  const cacheKey = app.url;
  if (cache.has(cacheKey)) {
    return res.end(cache.get(cacheKey));
  }

  renderer.renderToString(app, (err, html) => {
    if (err) {
      return res.status(500).end('Error rendering page');
    }
    cache.set(cacheKey, html);
    res.end(html);
  });
}

				
			

Caching improves performance by reducing the need for repeated rendering of the same pages.

SEO Considerations with SSR

SSR significantly improves SEO because it provides search engine crawlers with fully rendered HTML. However, there are a few additional steps you can take to optimize SEO:

  • Meta Tags: Ensure that dynamic meta tags (such as titles and descriptions) are rendered on the server.
  • Sitemap Generation: Consider generating a sitemap of your SSR pages to help search engines crawl your site.
  • Structured Data: Include structured data (like JSON-LD) in your SSR pages to improve how search engines display your content.

Common Pitfalls and Best Practices for SSR

Common Pitfalls

  • Slow Performance: SSR can slow down your server if not optimized correctly. Use caching to alleviate this.
  • Complex State Management: Managing the state between the server and client can become complex in large applications.
  • Initial Load Delay: While SSR improves time-to-content, it may still introduce a delay in rendering due to the time it takes for the server to generate the HTML.

Best Practices

  • Use Hydration Effectively: Ensure that your client-side Vue app properly hydrates after SSR, maintaining both performance and interactivity.
  • Minimize JavaScript on the Client Side: To maintain fast performance, avoid sending large JavaScript bundles to the client.
  • Consider Nuxt.js for Large Projects: If you are building a larger application, consider using Nuxt.js, which provides built-in support for SSR with best practices in mind.

Example of SSR with Vue and Nuxt.js

Nuxt.js simplifies SSR by providing an integrated framework. Here’s a basic setup:

1. Install Nuxt.js:

				
					npx create-nuxt-app my-ssr-app

				
			

2. Configure your app to enable SSR by default in nuxt.config.js:

				
					export default {
  ssr: true,  // Enable SSR
  target: 'server', // Use server-side rendering
}

				
			

3. Run the Nuxt app:

				
					npm run dev

				
			

Nuxt automatically handles routing, state management, and SSR out of the box, making it easier to build large-scale SSR applications with Vue.

Server-Side Rendering (SSR) in Vue.js is an essential tool for building high-performance, SEO-friendly applications. By rendering content on the server and delivering fully-rendered HTML to the client, SSR improves load times and makes content more accessible to search engines. While SSR introduces complexity, such as managing asynchronous data and caching, the benefits of improved SEO, performance, and user experience make it worth the investment for many applications. Happy Coding!❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India