render
Module for rendering React application on the server and in the browser
Overview
Module contains the logic for generating HTML pages, starting from getting current page component, and finishing with the rendering result HTML using the @tinkoff/htmlpagebuilder
library.
This module includes code for creating top-level React component with all necessary providers composition, and page and layout components from the current route.
Installation
You need to install @tramvai/module-render
- npm
- Yarn
npm install @tramvai/module-render
yarn add @tramvai/module-render
And connect to the project
import { createApp } from '@tramvai/core';
import { RenderModule } from '@tramvai/module-render';
createApp({
name: 'tincoin',
modules: [RenderModule],
});
Explanation
React Strict Mode
More information about Strict Mode can be found in the official documentation.
To set the mode, you must pass the useStrictMode
parameter when initializing the RenderModule
.
RenderModule.forRoot({ useStrictMode: true });
Application static assets
For static assets (JS, CSS, fonts, etc.) we create special resources registry module, which allow to provide in DI list of resources, and then render them to specifics slots in final HTML.
Example:
createApp({
providers: [
{
provide: RENDER_SLOTS,
multi: true,
useValue: [
{
type: ResourceType.inlineScript, // inlineScript wrap payload in tag <script>
slot: ResourceSlot.HEAD_CORE_SCRIPTS, // define position where in HTML will be included resource
payload: 'alert("render")',
},
{
type: ResourceType.asIs, // asIs just add payload as a string, without special processing
slot: ResourceSlot.BODY_TAIL,
payload: '<div>hello from render slots</div>',
},
],
},
],
});
- type - presets for different resources types
- slot - slot in HTML where resource will be included
- payload - information that will be rendered
Available slots
export const REACT_RENDER = 'react:render';
export const HEAD_PERFORMANCE = 'head:performance';
export const HEAD_META = 'head:meta';
export const HEAD_POLYFILLS = 'head:polyfills';
export const HEAD_CORE_STYLES = 'head:core-styles';
export const HEAD_CORE_SCRIPTS = 'head:core-scripts';
export const HEAD_DYNAMIC_SCRIPTS = 'head:dynamic-scripts';
export const HEAD_ANALYTICS = 'head:analytics';
export const HEAD_ICONS = 'head:icons';
export const BODY_START = 'body:start';
export const BODY_END = 'body:end';
export const BODY_TAIL_ANALYTICS = 'body:tail:analytics';
export const BODY_TAIL = 'body:tail';
Layout of slots in the HTML page
import type { StaticDescriptor, DynamicDescriptor } from '@tinkoff/htmlpagebuilder';
import { dynamicRender, staticRender } from '@tinkoff/htmlpagebuilder';
import { ResourceSlot } from '@tramvai/tokens-render';
import { formatAttributes } from './utils';
const {
REACT_RENDER,
HEAD_CORE_SCRIPTS,
HEAD_DYNAMIC_SCRIPTS,
HEAD_META,
HEAD_POLYFILLS,
HEAD_CORE_STYLES,
HEAD_PERFORMANCE,
HEAD_ANALYTICS,
BODY_START,
BODY_END,
HEAD_ICONS,
BODY_TAIL_ANALYTICS,
BODY_TAIL,
} = ResourceSlot;
export const htmlPageSchemaFactory = ({
htmlAttrs,
}): Array<StaticDescriptor | DynamicDescriptor> => {
return [
staticRender('<!DOCTYPE html>'),
staticRender(`<html ${formatAttributes(htmlAttrs, 'html')}>`),
staticRender('<head>'),
staticRender('<meta charset="UTF-8">'),
dynamicRender(HEAD_META),
dynamicRender(HEAD_PERFORMANCE),
dynamicRender(HEAD_CORE_STYLES),
dynamicRender(HEAD_POLYFILLS),
dynamicRender(HEAD_DYNAMIC_SCRIPTS),
dynamicRender(HEAD_CORE_SCRIPTS),
dynamicRender(HEAD_ANALYTICS),
dynamicRender(HEAD_ICONS),
staticRender('</head>'),
staticRender(`<body ${formatAttributes(htmlAttrs, 'body')}>`),
dynamicRender(BODY_START),
// react app
dynamicRender(REACT_RENDER),
dynamicRender(BODY_END),
dynamicRender(BODY_TAIL_ANALYTICS),
dynamicRender(BODY_TAIL),
staticRender('</body>'),
staticRender('</html>'),
];
};
How to add assets loading to a page
Automatic resource inlining
Concept
A large number of resource files creates problems when loading the page, because the browser has to create a lot of connections to small files
Solution
To optimize page loading, we've added the ability to include some resources directly in the incoming HTML from the server. To avoid inlining everything at all, we've added the ability to set an upper limit for file size.
Connection and configuration
Since version 0.60.7
inlining for styles is enabled by default, CSS files smaller than 40kb before gzip (+-10kb after gzip) are inlined.
To override these settings, add a provider specifying types of resources to be inlined (styles and/or scripts) and an upper limit for file size (in bytes, before gzip):
import { RESOURCE_INLINE_OPTIONS } from '@tramvai/tokens-render';
import { ResourceType } from '@tramvai/tokens-render';
import { provide } from '@tramvai/core';
provide({
provide: RESOURCE_INLINE_OPTIONS,
useValue: {
types: [ResourceType.script, ResourceType.style], // Turn on for a CSS and JS files
threshold: 1024, // 1kb unzipped
},
}),
Under the hood resource inliner cached all fetched resources.
By default cache size are:
- 300 - for fetched content
- 300 - for content sizes
- 300 - for disabled urls of unavailable resources
You can configure cache sizes with cacheSize
:
import { RESOURCE_INLINE_OPTIONS } from '@tramvai/tokens-render';
import { ResourceType } from '@tramvai/tokens-render';
import { provide } from '@tramvai/core';
provide({
provide: RESOURCE_INLINE_OPTIONS,
useValue: {
types: [ResourceType.script, ResourceType.style], // Turn on for a CSS and JS files
threshold: 1024, // 1kb unzipped
cacheSize: {
files: 50,
size: 100,
disabledUrl: 150,
},
},
}),
Be aware of memory consumption of resources - they are uncompressed. So with 5kb threshold and 1000 entities in cache you will get +5Mb on the heap.
Peculiarities
All scripts and styles (depending on the settings) registered through the ResourcesRegistry
are inlined.
File uploading to the server occurs in lazy mode, asynchronously. This means that there will be no inlining when the page first loads. It also means that there is no extra waiting for resources to load on the server side. Once the file is in the cache it will be inline. The cache has a TTL of 30 minutes and there is no resetting of the cache.
Automatic resource preloading
To speed up data loading, we've added a preloading system for resources and asynchronous chunks, which works according to the following scenario:
- After rendering the application, we get information about all the CSS, JS bundles and asynchronous chunks used in the application
- Next we add all the CSS to the preload tag and add onload event on them. We need to load the blocking resources as quickly as possible.
- When loading any CSS file, onload event will be fired (only once time) and add all preload tags to the necessary JS files
Layouts
How to
How to add assets loading to a page
There are 2 main ways how you can add resources to your application
- The
RENDER_SLOTS
token, where you can pass a list of resources, such as HTML markup, inline scripts, script tag - Token
RESOURCES_REGISTRY
to get the resource manager, and register the desired resources manually
Example:
Application example
import React from 'react';
import { createApp, createBundle, commandLineListTokens } from '@tramvai/core';
import {
RENDER_SLOTS,
RESOURCES_REGISTRY,
ResourceType,
ResourceSlot,
} from '@tramvai/module-render';
import { modules } from '../common';
function Page() {
return <div>Render</div>;
}
const bundle = createBundle({
name: 'mainDefault',
components: {
pageDefault: Page,
},
});
createApp({
name: 'render-add-resources',
modules: [...modules],
providers: [
{
// If you want to add your own resources (scripts, styles, images) for loading,
// you can use the provider RENDER_SLOTS to add the necessary assets,
// all this will then be used in the RenderModule and inserted into HTML
provide: RENDER_SLOTS,
multi: true,
useValue: [
{
type: ResourceType.inlineScript, // inlineScript wrap payload in tag <script>
slot: ResourceSlot.HEAD_CORE_SCRIPTS, // define position where in HTML will be included resource
payload: 'alert("render")',
},
{
type: ResourceType.asIs, // asIs just add payload as a string, without special processing
slot: ResourceSlot.BODY_TAIL,
payload: '<div>hello from render slots</div>',
},
],
},
{
provide: commandLineListTokens.resolveUserDeps,
multi: true,
// You can also add resources separately via DI and the RESOURCES_REGISTRY token
useFactory: ({ resourcesRegistry }) => {
return function addMyScripts() {
resourcesRegistry.register({
type: ResourceType.script, // script will create new script tag with src equal to payload
slot: ResourceSlot.HEAD_ANALYTICS, // define position where in HTML will be included resource
payload: './some-script.js',
});
};
},
deps: {
resourcesRegistry: RESOURCES_REGISTRY,
},
},
],
bundles: {
mainDefault: () => Promise.resolve({ default: bundle }),
},
});
React 18 concurrent features
tramvai
will automatically detect React version, and use hydrateRoot API on the client for 18+ version.
Before switch to React 18, we recommended to activate Strict Mode in your application. In Strict Mode which React warns about using the legacy API.
To connect, you must configure the RenderModule
:
modules: [
RenderModule.forRoot({ useStrictMode: true })
]
Testing
Testing render extensions via RENDER_SLOTS or RESOURCES_REGISTRY tokens
If you have a module or providers that define RENDER_SLOTS
or use RESOURCES_REGISTRY
, it is convenient to use special utilities to test them separately
import {
RENDER_SLOTS,
ResourceSlot,
RESOURCES_REGISTRY,
ResourceType,
} from '@tramvai/tokens-render';
import { testPageResources } from '@tramvai/module-render/tests';
import { CustomModule } from './module';
import { providers } from './providers';
describe('testPageResources', () => {
it('modules', async () => {
const { render } = testPageResources({
modules: [CustomModule],
});
const { head } = render();
expect(head).toMatchInlineSnapshot(`
"
<meta charset=\\"UTF-8\\">
<script>console.log(\\"from module!\\")</script>
"
`);
});
it('providers', async () => {
const { render, runLine } = testPageResources({
providers,
});
expect(render().body).toMatchInlineSnapshot(`
"
"
`);
await runLine(commandLineListTokens.resolvePageDeps);
expect(render().body).toMatchInlineSnapshot(`
"
<script defer=\\"defer\\" charset=\\"utf-8\\" crossorigin=\\"anonymous\\" src=\\"https://scripts.org/script.js\\"></script>
<span>I\`m body!!!</span>
"
`);
});
});