DEV Community

Iwanttoeatfish
Iwanttoeatfish

Posted on • Edited on

ArkTS Server-side Rendering (SSR)

ArkTS Server-Side Rendering (SSR) Practices

I. Introduction

In today's front-end development landscape, server-side rendering (SSR) is gaining traction for building high-performance web applications. ArkTS, with its strong expressiveness and rich features, enhances user experience when combined with SSR. This article explores ArkTS SSR practices, covering concepts, benefits, technical choices, development processes, code implementation, deployment, and performance optimization.

II. Concept and Benefits of SSR

2.1 Improved First-Screen Loading Speed

In traditional client-side rendering (CSR), users see a blank page until JavaScript loads and executes. With SSR, the server sends a complete HTML page to the browser, eliminating wait time for JavaScript and significantly reducing first-screen loading time.

2.2 Enhanced SEO

Search engine crawlers primarily parse static HTML content. In CSR, dynamic content generated by JavaScript may not be parsed correctly, affecting search rankings. SSR delivers complete HTML pages, making content easily crawlable and improving SEO.

III. Technical Choices for ArkTS SSR

3.1 Setting Up a Server Environment with Node.js

Node.js, based on Chrome's V8 engine, offers an efficient environment for server-side applications. To set up an ArkTS SSR project:

mkdir arktss-ssr-project
cd arktss-ssr-project
npm init -y
Enter fullscreen mode Exit fullscreen mode

3.2 Framework Selection and Integration

Vue.js is a good choice for ArkTS SSR development. Install the required dependencies:

npm install vue @vue/server-renderer arkts-loader --save
Enter fullscreen mode Exit fullscreen mode

Create an ArkTS component (HelloWorld.ets):

import { defineComponent } from 'vue';

export default defineComponent({
  setup() {
    return () => (
      <h1>Hello, World!</h1>
    );
  },
});
Enter fullscreen mode Exit fullscreen mode

IV. Development Process and Code Implementation

4.1 Server-Side Component Rendering

Use @vue/server-renderer to render components on the server. Create a server.js file:

const express = require('express');
const { createSSRApp } = require('vue');
const { renderToString } = require('@vue/server-renderer');
const HelloWorld = require('./HelloWorld.ets').default;

const app = express();

app.get('/', async (req, res) => {
const vueApp = createSSRApp(HelloWorld);
const html = await renderToString(vueApp);
const page =
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ArkTS SSR Example</title>
</head>
<body>
${html}
<script src="/client.js"></script>
</body>
</html>
;
res.send(page);
});

const port = 3000;
app.listen(port, () => {
console.log(Server is running on port ${port});
});

4.2 Building an Isomorphic Rendering Pipeline

In production, sending only the initial HTML from the server is not enough; the browser must “take over” the page and make it interactive. This process is called hydration. To let Vue run both on the server and in the browser, you need to bundle the application twice: one bundle for renderToString and one for the browser entry.

4.2.1 Browser-Side Entry client.js

import { createSSRApp } from 'vue';
import HelloWorld from './HelloWorld.ets';

// Create an app instance identical to the server one
const app = createSSRApp(HelloWorld);
// Mount to the DOM and trigger hydration
app.mount('#app');
Enter fullscreen mode Exit fullscreen mode

The server-rendered HTML must contain a mount point <div id="app">; otherwise the browser cannot locate the root node and hydration will fail.

4.2.2 Build-Script Configuration

Use Vite or Webpack for dual-target bundling. Below is a minimal Vite 4 example:

vite.config.ts

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import arkts from 'arkts-loader/vite';

export default defineConfig({
  plugins: [vue(), arkts()],
  build: {
    rollupOptions: {
      input: {
        client: './src/client.ts',
        server: './src/server.ts',
      },
      output: {
        dir: 'dist',
        format: 'cjs', // Server needs CommonJS
      },
    },
  },
  ssr: {
    target: 'node',
  },
});
Enter fullscreen mode Exit fullscreen mode

After the build, dist/client.js is referenced by a <script> tag, while dist/server.js serves as the Node entry.

4.3 Isomorphic Routing & State Management

4.3.1 Introducing Vue Router

Install the dependency:

npm i vue-router@4
Enter fullscreen mode Exit fullscreen mode

Create router.ts:

import { createMemoryHistory, createRouter, createWebHistory } from 'vue-router';
import Home from './pages/Home.ets';
import About from './pages/About.ets';

export default function createHistory(isServer = false) {
  return isServer
    ? createMemoryHistory()
    : createWebHistory();
}

export const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
];

export function createVueRouter(isServer = false) {
  return createRouter({
    history: createHistory(isServer),
    routes,
  });
}
Enter fullscreen mode Exit fullscreen mode

4.3.2 Pre-Fetching Data on the Server

Using Pinia (or Vuex) as an example, fetch API data before rendering and serialize it into the HTML:

// server.ts snippet
import { renderToString } from '@vue/server-renderer';
import { createPinia } from 'pinia';
import { createSSRApp } from 'vue';
import createVueRouter from './router';
import { setActivePinia } from 'pinia';
import { useStore } from './stores/main';

export async function render(url: string) {
  const app = createSSRApp(App);
  const router = createVueRouter(true);
  const pinia = createPinia();
  app.use(router).use(pinia);

  // Let router match the current URL first
  await router.push(url);
  await router.isReady();

  // Collect asyncData hooks from matched components
  const matched = router.currentRoute.value.matched;
  const prefetchFns = matched.map(r => r.components?.default?.asyncData).filter(Boolean);
  await Promise.all(prefetchFns.map(fn => fn({ store: useStore() })));

  const html = await renderToString(app);
  // Inject state into window.__INITIAL_STATE__
  const state = `<script>window.__INITIAL_STATE__=${JSON.stringify(pinia.state.value)}</script>`;
  return { html, state };
}
Enter fullscreen mode Exit fullscreen mode

In client.ts, deserialize __INITIAL_STATE__ to avoid duplicate requests.

4.3.3 Error Boundaries & Fallback Strategy

To prevent SSR failures from causing a blank white page, wrap the render call in a try-catch and fall back to CSR:

try {
  const { html, state } = await render(req.url);
  res.send(template.replace('<!--app-->', html).replace('<!--state-->', state));
} catch (e) {
  console.error('SSR error:', e);
  // Return a pure CSR page
  res.send(template.replace('<!--app-->', '<div id="app"></div>').replace('<!--state-->', ''));
}
Enter fullscreen mode Exit fullscreen mode

4.4 Performance Optimization Checklist

  • Caching: Use an LRU cache for static-route results to avoid duplicate renders.
  • Streaming: @vue/server-renderer supports renderToNodeStream, which improves TTFB by flushing HTML chunks early.
  • Inlining: Inline critical CSS and first-screen data inside <style> and <script> tags to reduce round-trips.
  • CDN: Upload dist/client assets to a CDN and set long-term Cache-Control headers (e.g., max-age=31536000, immutable).

Top comments (0)