Velo: Server Side Rendering and Warmup Data APIs

shoonia

Alexander Zaytsev

Posted on July 22, 2022

Velo: Server Side Rendering and Warmup Data APIs

Learn how to optimize data receiving and reduce the Wix site load time

In Velo, we use the $w.onReady() method as a start point for interacting with the page. This method ensures that all the page elements have finished loading and we can interact with them. The life cycle of the Velo site includes two runs of the $w.onReady() method.

The first run of the $w.onReady() callback happens on the server-side when the server builds the HTML page. The server executes a Velo code and puts a result into HTML (if it's possible).

The second run goes on the client-side in the browser when a site page has loaded.

Velo API Reference: Rendering

When possible, the rendering process is split in two in order to improve performance. The first cycle in the process happens in the server-side code and the second cycle happens in the client-side code. If not possible on the server-side, all rendering happens client-side.

Let's playing with SSR for understanding how it works.

For example, we have the below code:

$w.onReady(function () {
  $w('#text1').text = 'Hello!';
});
Enter fullscreen mode Exit fullscreen mode

Code will be executed on the server-side then a result will be added to the HTML page. And the page will send to the client-side with inserted data.

Rendering API

We can control the step of the render cycle with wixWindow.rendering.env API.

env property returns backend when rendering on the server-side and browser when rendering on the client-side.

Let's update the code to see it. It's a string with env value and timestamp.

import { rendering } from 'wix-window';

$w.onReady(function () {
  $w('#text1').text = `${rendering.env}: ${Date.now()}`;
});
Enter fullscreen mode Exit fullscreen mode

Now, when we reload the page we can see that HTML content has backend value. When the page finished loading then we see the browser value, it's the second run of $w.onReady() on the client-side updates a value of text.

SSR & browser runtime

It looks easy.

Asynchronous operation

What about async operations?

If we want to add SSR with some async operation, we should wait for the promise to be fulfilled.

Let's have a look at an example. Creates a query for retrieving items from a database and prints them as a string.

import wixData from 'wix-data';

$w.onReady(function () {
  // Async request to database
  wixData.query('goods').find()
    .then((data) => {
      $w('#text1').text = JSON.stringify(data.items);
    });
});
Enter fullscreen mode Exit fullscreen mode

As we can see, the SSR doesn't work with any async operations. When we reload the page, we see a default text that the Text element contains in the editor. And after a while, we see database items. It's the second run of the $w.onReady() callback on the client-side.

A site without server-side render with dynamic data

I'm using the throttling of the network in Chrome DevTools to reduce Internet speed. It may be helpful for debugging.

It happened because $w.onReady() doesn't wait for a promise fulfilled on the server-side. The server doesn't wait for a query result and sends the HTML page with default content.

To fix is very simple, we should wait for a promise result. The $w.onReady() supports the async callback functions. Let's update the code with async/await operators.

import wixData from 'wix-data';

$w.onReady(async function () {
  const data = await wixData.query('goods').find();

  $w('#text1').text = JSON.stringify(data.items);
});
Enter fullscreen mode Exit fullscreen mode

Now, we can see the SSR starts to work. And the server has rendered the HTML page with the database items.

A site with server-side render for dynamic data

ā— don't forget to turn off the throttling of the network after testing šŸ˜‰

Long async calls slow down site performance

We should be careful using $w.onReady() with an async callback. Long async tasks slow down the render the the page.

For example, we add a promise with a delay of 5 seconds into the callback.

$w.onReady(async function () {
  // a delay for 5 seconds
  await new Promise((r) => setTimeout(r, 5000));

  $w('#text1').text = Date.now().toString();
});
Enter fullscreen mode Exit fullscreen mode

If we run this code, we will see that the server will wait for 5 seconds to complete. And after the HTML page has loaded on the client, we again wait for 5 seconds before seeing the result.

We wait twice for the promise to be fulfilled on the server and on the client.

chrome network inspector

Network inspector, time to load the HTML page from the server with a delay of 5 seconds

The Warmup Data API

Using the Warmup Data API, we are able to transfer data with a page code from the server and read this data on the client.

Velo API Reference:

The Warmup Data API is used to optimize data loading for sites that render both on the server and in the browser, allowing costly data fetching operations to be done only once.

Velo: The Warmup Data API example

import { warmupData, rendering } from 'wix-window';

// Set data on the server-side
if (rendering.env === 'backend') {
  warmupData.set('my-key', 'server data');
}

// Get data on the client-side
if (rendering.env === 'browser') {
  const data = warmupData.get('my-key');

  console.log(data); // -> "server data"
}
Enter fullscreen mode Exit fullscreen mode

We can use the Warmup Data to reduce requests to a database. There we save the query response to warmupData on the server and read it on the client without additional database request.

Implement Warmup Data util function

We'll implement a feature that will enable server-side rendering and use the Warmup Data to prevent second data request on the client-side, it reduces a time of waiting.

Create a file for util function.

Add file to public section on the sidebar

public
ā””ā”€ā”€ warmupUtil.js
Enter fullscreen mode Exit fullscreen mode

It is a wrapper function. It has two arguments:

  • First argument - key: It's a unique key corresponding to data for the Warmup Data
  • Second argument - func: It's an async function which result we want to use with the Warmup Data.

public/warmupUtil.js

import { warmupData, rendering } from 'wix-window';

export const warmupUtil = async (key, func) => {
  // On the server-side
  if (rendering.env === 'backend') {
    // Get data
    const data = await func();

    // Set the warmup data on the server-side
    warmupData.set(key, data);

    return data;
  }

  // On the client-side

  // Get the warmup data on the client-side
  const data = warmupData.get(key);

  // Checking a cached data exist
  if (data) {
    return data;
  }

  // If we don't have cache data from the server,
  // then we do a backup call on the client
  return func();
};
Enter fullscreen mode Exit fullscreen mode

On the server, it waits for the async function result and sets it to the Warmup Data.

On the client, it uses data from the Warmup Data. If it has no data (some glitch on the server), it will call func on the client.

Parallel execution for a few async tasks

We should remember the $w.onReady() effect of page loading. If we want to use a few async functions in $w.onReady() callback then we should avoid using them in queue one by one.

For example, if each of these async functions executes at 100 milliseconds, the $w.onReady() will need to wait for 300 milliseconds for complete execution all of them.

// āŒ wrong approach!!
$w.onReady(async function () {
  const one = await warmupUtil('one-async-func', oneAsyncFunc); // ā³ 100 ms
  const two = await warmupUtil('two-async-func', twoAsyncFunc); // ā³ 100 ms
  const three = await warmupUtil('three-async-func', threeAsyncFunc); // ā³ 100 ms

  // ā³ wait one by one (100 ms * 3) = 300 ms

  $w('#text1').text = JSON.stringify({ one, two, three });
});
Enter fullscreen mode Exit fullscreen mode

We are able to aggregate a bunch of promises with Promise.all(), execute them in parallel, and wait until all of them are ready.

// āœ… parallel asynchronous execution
$w.onReady(async function () {
  const [one, two, three] = await Promise.all([
    warmupUtil('one-async-func', oneAsyncFunc),
    warmupUtil('two-async-func', twoAsyncFunc),
    warmupUtil('three-async-func', threeAsyncFunc),
  ]);

  // ā³ wait 100 ms. Parallel execution of all promises

  $w('#text1').text = JSON.stringify({ one, two, three });
});
Enter fullscreen mode Exit fullscreen mode

Code Snippets

Here is a code snippet with JSDoc annotation. And an example of use.

public/warmupUtil.js

import { warmupData, rendering } from 'wix-window';

/**
 * @template T
 * @param {string} key
 * @param {() => Promise<T>} func
 * @returns {Promise<T>}
 */
export const warmupUtil = async (key, func) => {
  if (rendering.env === 'backend') {
    const data = await func();

    warmupData.set(key, data);

    return data;
  }

  const data = warmupData.get(key);

  if (data) {
    return data;
  }

  return func();
};
Enter fullscreen mode Exit fullscreen mode

Page Code Tab

import { warmupUtil } from 'public/warmupUtil';

const getGoods = async () => {
   const { items } = await wixData.query('goods').find();

   return items;
};

$w.onReady(async function () {
  const items = await warmupUtil('goods-items', getGoods);

  $w('#text1').text = JSON.stringify(items);
});
Enter fullscreen mode Exit fullscreen mode

Resources

Posts

šŸ’– šŸ’Ŗ šŸ™… šŸš©
shoonia
Alexander Zaytsev

Posted on July 22, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related