Velo: Server Side Rendering and Warmup Data APIs
Learn how to optimize data retrieval and reduce the Wix site load time
In Velo, we use the $w.onReady()
method as a starting point for interacting with the page. This method ensures that all the page elements have finished loading and we can interact with them. The lifecycle 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 Velo code and puts a result into HTML (if it's possible).
The second run occurs on the client-side in the browser after a site page has loaded.
Let's playing with SSR for understanding how it works.
For example, we have the below code:
$w.onReady(function () {
$w('#text1').text = 'Hello!';
});
Code will be executed on the server-side, then a result will be added to the HTML page. The page will then be sent to the client-side with the 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-frontend';
$w.onReady(function () {
$w('#text1').text = `${rendering.env}: ${Date.now()}`;
});
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 that updates the value of text.
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. Create a query for retrieving items from a database and print 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);
});
});
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
It happened because $w.onReady()
doesn't wait for a promise to be fulfilled on the server-side. The server doesn't wait for a query result and sends the HTML page with default content.
To fix it 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);
});
Now, we can see the SSR starts to work. And the server has rendered the HTML page with the database items.
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 of the page.
For example, we add a timeout 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();
});
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 will again wait for 5 seconds before seeing the result.
We wait twice for the promise to be fulfilled on the server and the client.
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 side.
We can use the Warmup Data to reduce requests to a database. We save the query response to warmupData
on the server and read it on the client without additional database requests.
Implement Warmup Data util function
We'll implement a feature that will enable server-side rendering and use the Warmup Data to prevent a second data request on the client-side, reducing the waiting time.
Create a file for util function.
Add file to public section on the sidebar
It is a wrapper function. It has two arguments:
- First argument - key
- It's a unique key corresponding to the data for the Warmup Data.
- Second argument - func
- It's an async function whose result we want to use with the the Warmup Data.
public/warmupUtil.js
import { warmupData, rendering } from 'wix-window-frontend';
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 cached data from the server,
// then we do a backup call on the client
return func();
};
On the server, it waits for the asynchronous function result and sets it to the Warmup Data.
On the client, it uses data from the Warmup Data. If it has no data (due to 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, we should avoid using them in a queue one by one.
For example, if each of these async functions executes in 100 milliseconds, the $w.onReady()
will need to wait for 300 milliseconds for the complete execution of 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 });
});
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 });
});
Code Snippets
Here is a code snippet with JSDoc annotations, and an example of its use.
public/warmupUtil.js
import { warmupData, rendering } from 'wix-window-frontend';
/**
* @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();
};
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);
});