Рендеринг на стороне сервера React (разделение кода и предварительная выборка данных)

React.js

Базовый скелет и маршрут проекта были построены в предыдущих разделах, но этого недостаточно для фактической разработки проекта. По мере роста бизнеса увеличивается и код последовательности прикладного уровня.Если все коды упаковать в один файл, первая загрузка приведет к относительно большему времени и увеличению трафика (для мобильного терминала). Приложение содержит много страниц, и пользователь будет посещать только одну страницу за раз, код страницы, не посещаемой пользователем, не должен загружаться перед посещением, а ресурсы, необходимые для загрузки страницы, должны быть изменены только тогда, когда пользователь посещения. Ранее построенные проекты пока не связаны с взаимодействием с данными.Основой бизнеса являются данные.В этом разделе будет представлена ​​сегментация кода на основе маршрутизации, взаимодействие данных и синхронизация.

Предыдущий раздел:Изоморфизм внешней и внутренней маршрутизации

См. адрес исходного кода в конце статьи.

Часть кода в этом разделе была переписана, нажмите, чтобы узнать подробностиздесь

разделение кода

Отложенная загрузка маршрута

Существует множество решений при разделении кода, напримерreact-loadable,react-async-component,loadable-components, все три поддерживают разделение кода и отложенную загрузку, а также все поддерживают рендеринг на стороне сервера. Когда react-loadable и react-async-component выполняют рендеринг на стороне сервера, шаги очень громоздки, loadable-components предоставляет простые операции для поддержки рендеринга на стороне сервера, а здесь используются loadable-components

Если вы используете webpack4, используйте загружаемые компонентыновая версия,здесьэто полный пример, переписанный для использования новой версии

Установить загружаемые компоненты

npm install loadable-components

Измените компоненты в конфигурации маршрутизации на динамический импорт.

src/router/index.js

import Loadable from "loadable-components";

const router = [
  {
    path: "/bar",
    component: Loadable(() => import("../views/Bar"))
  },
  {
    path: "/baz",
    component: Loadable(() => import("../views/Baz"))
  },
  {
    path: "/foo",
    component: Loadable(() => import("../views/Foo"))
  },
  {
    path: "/top-list",
    component: Loadable(() => import("../views/TopList")),
    exact: true
  }
];

import()Динамический импорт — это синтаксис, поддерживаемый начиная с Webpack2, который по существу использует промисы.Если вы хотите работать в старых браузерах, вам нужноes6-promiseилиpromise-polyfill

для разбораimport()Синтаксис, вам нужно настроить плагин babelsyntax-dynamic-import, тогда он будет работать в одностраничном приложении. Здесь загружаемые компоненты используются для рендеринга на стороне сервера, а конфигурация babel выглядит следующим образом.

"plugins": [
  "loadable-components/babel"
]

Примечание. Здесь используется версия babel6.x.

использовать на стороне клиентаloadComponentsМетод загружает компонент, а затем устанавливает его. Запись клиента изменяется следующим образом

src/entry-client.js

import { loadComponents } from "loadable-components";
import App from "./App";

// 开始渲染之前加载所需的组件
loadComponents().then(() => {
  ReactDOM.hydrate(<App />, document.getElementById("app"));
});

вызов сервераgetLoadableState()Затем вставьте состояние в html-фрагмент

src/server.js

const { getLoadableState } = require("loadable-components/server");

...

let component = createApp(context, req.url);
// 提取可加载状态
getLoadableState(component).then(loadableState => {
  let html = ReactDOMServer.renderToString(component);

  if (context.url) {  // 当发生重定向时,静态路由会设置url
    res.redirect(context.url);
    return;
  }

  if (!context.status) {  // 无status字段表示路由匹配成功
    // 获取组件内的head对象,必须在组件renderToString后获取
    let head = component.type.head.renderStatic();
    // 替换注释节点为渲染后的html字符串
    let htmlStr = template
    .replace(/<title>.*<\/title>/, `${head.title.toString()}`)
    .replace("<!--react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()}`)
    .replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>\n${loadableState.getScriptTag()}`);
    // 将渲染后的html字符串发送给客户端
    res.send(htmlStr);
  } else {
    res.status(context.status).send("error code:" + context.status);
  }
});

передачаgetLoadableState()Передайте корневой компонент, дождитесь загрузки состояния, рендеринга и вызоваloadableState.getScriptTag()Вставьте возвращенный скрипт в html-шаблон

Требуется рендеринг на стороне сервераmodulesопции

const AsyncComponent = loadable(() => import('./MyComponent'), {
  modules: ['./MyComponent'],
})

Эту опцию не нужно прописывать вручную, используйтеloadable-components/babelПодойдет плагин.import()Синтаксис не поддерживается в узле, поэтому на сервере также необходимо настроить плагинdynamic-import-node

Установитьdynamic-import-node

npm install babel-plugin-dynamic-import-node --save-dev

Клиент не нуждается в этом плагине.Далее измените конфигурацию веб-пакета и используйте клиент.babelrcфайл, сервер через загрузчикoptionsвозможность указать конфигурацию Babel

Будуwebpack.config.base.jsСледующая конфигурация перемещена вwebpack.config.client.jsсередина

{
  test: /\.(js|jsx)$/,
  loader: ["babel-loader", "eslint-loader"],
  exclude: /node_modules/
}

webpack.config.client.js

rules: [
  {
    test: /\.(js|jsx)$/,
    loader: ["babel-loader", "eslint-loader"],
    exclude: /node_modules/
  },
  ...util.styleLoaders({
    sourceMap: isProd ? true : false,
    usePostCSS: true,
    extract: isProd ? true : false
  })
]

Конфигурация пакета сервера изменена следующим образом

webpack.config.server.js

rules: [
  {
    test: /\.(js|jsx)$/,
    use: [
      {
        loader: "babel-loader",
        options: {
          babelrc: false,
          presets: [
            "react",
            [
              "env",
              { "targets": { "node": "current" } }
            ]
          ],
          "plugins": [ "dynamic-import-node", "loadable-components/babel" ]
        }
      },
      { loader: "eslint-loader" }
    ],
    exclude: /node_modules/
  },
  ...util.styleLoaders({
    sourceMap: true,
    usePostCSS: true,
    extract: true
  })
]

бегатьnpm run dev, откройте браузер и введитеhttp://localhost:3000, вы можете увидеть загрузку сначала в сетевой панелиapp.b73b88f66d1cc5797747.js, а затем загрузите js, необходимый для текущей страницы бара (та, что на рисунке ниже3.b73b88f66d1cc5797747.js)

При выборе других маршрутов соответствующие js будут загружены и выполнены.

Оптимизация упаковки Webpack

В реальном использовании, при итеративном обновлении приложения, файл после запакованного файла будет становиться все больше и больше Основной файл скриптаapp.xxx.jsОн содержит сторонние модули и бизнес-код.Бизнес-код может измениться в любое время, в то время как сторонние модули останутся в основном неизменными в течение определенного периода времени, если вы не обновите фреймворк или библиотеку, которую используете в настоящее время.app.xxx.jsххх использовать вchunkhashимя,chunkhashУказывает хэш содержимого чанка, чанк стороннего модуля не изменится, мы его выделяем для удобства кеширования браузером

Для получения дополнительной информации о output.filename, пожалуйста, отметьтездесь

Чтобы извлечь сторонние модули, вам нужно использовать собственный webpackCommonsChunkPluginплагин, а для лучшего кеширования извлекаем модуль начальной загрузки webpack в отдельный файл

webpack.config.client.js

plugins: [
  ...
  new webpack.optimize.CommonsChunkPlugin({
    name: "vendor",
    minChunks: function(module) {
      // 阻止.css文件资源打包到vendor chunk中
      if(module.resource && /\.css$/.test(module.resource)) {
        return false;
      }
      // node_modules目录下的模块打包到vendor chunk中
      return module.context && module.context.includes("node_modules");
    }
  }),
  // 分离webpack引导模块
  new webpack.optimize.CommonsChunkPlugin({
    name: "manifest",
    minChunks: Infinity
  })
]

Благодаря вышеуказанной конфигурации пакет, содержащий сторонние модули, будет упакованvendor.xxx.jsа такжеmanifest.xxx.js

Примечание. Здесь используется версия webpack3.x, а плагин CommonsChunkPlugin удален из webpack4. Пожалуйста, используйте веб-пакет4SplitChunksPlugin

Проект используется только в режиме производстваchunkhash, затем запуститеnpm run buildПакет

Исправлятьsrc/App.jsxКод в , а затем упаковать его

можно увидетьvender.xxx.jsИмя файла не изменилось,app.xxx.jsИзменено, имена упакованных файлов 4-х асинхронных компонентов не изменились.mainfest.xxx.jsизменился

Предварительная выборка данных и синхронизация

Рендеринг на стороне сервера должен возвращать содержимое страницы с сервера клиенту.Если какое-то содержимое получено вызовом запроса интерфейса, то данные должны быть загружены заранее, затем отрендерены, а затем вызваныReactDOMServer.renderToString()Для отображения полной страницы HTML-контент, отображаемый клиентом, должен соответствовать HTML-контенту, возвращаемому сервером, что требует обеспечения согласованности данных на клиенте и данных на сервере.

Redux используется здесь для управления данными.Когда Redux выполняет рендеринг на стороне сервера, он создает новый Store для каждого запроса, а затем инициализирует состояние и возвращает его клиенту.Клиент получает это состояние и создает новый Store.

Рендеринг Redux на стороне сервераПример

Присоединяйтесь к Редукс

Установить связанные зависимости

npm install redux redux-thunk react-redux

Сначала создайте базовую структуру проекта Redux.

actionTypes.js

export const SET_TOP_LIST = "SET_TOP_LIST";

export const SET_TOP_DETAIL = "SET_TOP_DETAIL";

actions.js

import { SET_TOP_LIST, SET_TOP_DETAIL } from "./actionTypes";

export function setTopList(topList) {
  return { type: SET_TOP_LIST, topList };
}

export function setTopDetail(topDetail) {
  return { type: SET_TOP_DETAIL, topDetail };
}

reducers.js

import { combineReducers } from "redux";
import * as ActionTypes from "./actionTypes";

const initialState = {
  topList: [],
  topDetail: {}
}

function topList(topList = initialState.topList, action) {
  switch (action.type) {
    case ActionTypes.SET_TOP_LIST:
      return action.topList;
    default:
      return topList;
  }
}

function topDetail(topDetail = initialState.topDetail, action) {
  switch (action.type) {
    case ActionTypes.SET_TOP_DETAIL:
      return action.topDetail;
    default:
      return topDetail;
  }
}

const reducer = combineReducers({
  topList,
  topDetail
});

export default reducer;

store.js

import { createStore, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import reducer from "./reducers";

// 导出函数,以便客户端和服务端根据初始state创建store
export default (store) => {
  return createStore(
    reducer,
    store,
    applyMiddleware(thunkMiddleware) // 允许store能dispatch函数
  );
}

Здесь необходимо использовать данные запросаАсинхронное действие, Магазин по умолчанию может только отправлять объекты, используйтеredux-thunkПромежуточное ПО может выполнять функцию диспетчеризации

следующий вaction.jsНапишите функцию создания асинхронного действия в

import { getTopList, getTopDetail } from "../api";

...

export function fatchTopList() {
  // dispatch由thunkMiddleware传入
  return (dispatch, getState) => {
    return getTopList().then(response => {
      const data = response.data;
      if (data.code === 0) {
        // 获取数据后dispatch,存入store
        dispatch(setTopList(data.data.topList));
      }
    });
  }
}

export function fetchTopDetail(id) {
  return (dispatch, getState) => {
    return getTopDetail(id).then(response => {
      const data = response.data;
      if (data.code === 0) {
        const topinfo = data.topinfo;
        const top = {
          id: topinfo.topID,
          name: topinfo.ListName,
          pic: topinfo.pic,
          info: topinfo.info
        };
        dispatch(setTopDetail(top));
      }
    });
  }
}

Функция создания действия в приведенном выше коде возвращает функцию с асинхронным запросом, в котором могут быть отправлены другие действия. Здесь в этой функции вызывается запрос интерфейса.После завершения запроса данные сохраняются в состоянии через диспетчеризацию, а затем возвращается промис, чтобы после завершения асинхронного запроса можно было выполнить другую обработку. Для поддержки сервера и клиента в асинхронном запросе вы можете использоватьaxiosили использовать его в браузереfetch API, используется в узлеnode-fetch

Здесь в качестве источника данных используется интерфейс QQ Music, а сервер используетaxios, клиент не поддерживает междоменное использованиеjsonp,src/api/index.jsКод выглядит следующим образом

import axios from "axios";
import jsonp from "jsonp";

const topListUrl = "https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg";

if (process.env.REACT_ENV === "server") {
  return axios.get(topListUrl + "?format=json");
} else {
  // 客户端使用jsonp请求
  return new Promise((resolve, reject) => {
    jsonp(topListUrl + "?format=jsonp", {
      param: "jsonpCallback",
      prefix: "callback"
    }, (err, data) => {
      if (!err) {
        const response = {};
        response.data = data;
        resolve(response);
      } else {
        reject(err);
      }
    });
  });
}

Если вы хотите узнать больше о музыкальном интерфейсе QQ, нажмитездесь

Чтобы предоставить компонентам презентации React доступ к состоянию, нужно использоватьreact-reduxмодульныйconnectМетод подключается к Store и записывает компонент-контейнерTopList

src/containers/TopList.jsx

import { connect } from "react-redux"
import TopList from "../views/TopList";

const mapStateToProps = (state) => ({
    topList: state.topList
});

export default connect(mapStateToProps)(TopList);

существуетsrc/router/index.jsЦентральная ручка имеет оригиналimport("../views/TopList"))изменить наimport("../containers/TopList"))

{
  path: "/top-list",
  component: Loadable(() => import("../containers/TopList")),
  exact: true
}

дисплейный компонентTopListСостояние доступа через реквизиты в

class TopList extends React.Component {
  render() {
    const { topList } = this.props;
    return (
      <div>
        ...
        <ul className="list-wrapper">
          {
            topList.map(item => {
              return <li className="list-item" key={item.id}>
                {item.title}
              </li>;
            })
          }
        </ul>
      </div>
    )
  }
}

Далее вводим файл на стороне сервераentry-server.jsиспользуется вProviderпакетStaticRouterи экспортcreateStoreфункция

src/entry-server.js

import createStore from "./redux/store";
...

const createApp = (context, url, store) => {
  const App = () => {
    return (
      <Provider store={store}>
        <StaticRouter context={context} location={url}>
          <Root setHead={(head) => App.head = head}/>  
        </StaticRouter>
      </Provider>
    )
  }
  return <App />;
}

module.exports = {
  createApp,
  createStore
};

server.jsполучено вcreateStoreФункция создает Store без данных

let store = createStore({});

// 存放组件内部路由相关属性,包括状态码,地址信息,重定向的url
let context = {};
let component = createApp(context, req.url, store);

Клиент также используетProviderУпаковать, создать магазин без данных и передать

src/App.jsx

import createStore from "./redux/store";
...

let App;
if (process.env.REACT_ENV === "server") {
  // 服务端导出Root组件
  App = Root;
} else {
  const Provider = require("react-redux").Provider;
  const store = createStore({});
  App = () => {
    return (
      <Provider store={store}>
        <Router>
          <Root />
        </Router>
      </Provider>
    );
  };
}
export default App;

Предварительная выборка данных

Есть два способа получить данные.Первый — указать метод загрузки данных на маршруте, как показано ниже.

const routes = [
  {
    path: "/",
    component: Root,
    loadData: () => getSomeData()
  }
  ...
];

Другой способ — поместить метод загрузки данных в соответствующий компонент и определить его как статический метод, что более интуитивно понятно.

В данном примере используется второй подход.TopListСтатический метод определенный компонентasyncDataВходящее хранилище для отправки асинхронного действия, определенного здесь как статический метод, поскольку до того, как компонент еще не был создан недоступным экземпляромthis

static asyncData(store) {
  return store.dispatch(fatchTopList());
}

fatchTopListВозвращаемая функцияredux-thunkвызов промежуточного программного обеспечения,redux-thunkПромежуточное ПО будет передавать возвращаемое значение вызывающей функции в качестве возвращаемого значения метода отправки.

Теперь вам нужно получить компонент маршрутизации в момент запросаasyncDataметод и вызов, реагировать-маршрутизатор вreact-router-configМодуль предоставляет намmatchRoutesметод сопоставления маршрута в соответствии с конфигурацией маршрута

Чтобы использовать сопоставление маршрутов на сервере, необходимо изменить конфигурацию маршрута сentry-server.jsэкспорт в

src/entry-server.js

import { router } from "./router";
...

module.exports = {
  createApp,
  createStore,
  router
};

существуетserver.jsполучено вrouterКонфигурация маршрутизации, вызываемая при загрузке всех асинхронных компонентов.matchRoutes()Выполните сопоставление маршрутов и вызовите все соответствующие маршрутыasyncDataРендеринг после метода

let promises;
getLoadableState(component).then(loadableState => {
  // 匹配路由
  let matchs = matchRoutes(router, req.path);
  promises = matchs.map(({ route, match }) => {
    const asyncData = route.component.Component.asyncData;
    // match.params获取匹配的路由参数
    return asyncData ? asyncData(store, Object.assign(match.params, req.query)) : Promise.resolve(null);
  });

  // resolve所有asyncData
  Promise.all(promises).then(() => {
    // 异步数据请求完成后进行服务端render
    handleRender();
  }).catch(error => {
    console.log(error);
    res.status(500).send("Internal server error");
  });
  ...
}

используется в приведенном выше кодеroute.componentЧто получается, так это асинхронный компонент, возвращаемый загружаемыми компонентами,route.component.Componentэто реальный компонент маршрутизации, который должен быть вызванgetLoadableState()можно получить позже. если компонент существуетasyncDataспособ поставитьpromisesВ массиве, если он не существует, верните разрешенный промис, а затем разрешите все промисы. некоторые URL-адреса похожи/path/:id,match.paramsиспользуется для получения URL в:idПредставленные параметры. Если какие-то параметры передаются в виде ?, их можно передать черезreq.queryполучить, слиться вmatch.params, передается компоненту для обработки

Примечание. Используйте второй параметр в matchRoutes.req.path,req.pathПолученный URL-адрес не содержит параметра запроса, поэтому его можно правильно сопоставить.

Синхронные данные

Сервер запрашивает данные заранее и сохраняет их в Store.Клиент инициализирует экземпляр Store в соответствии с этим состоянием, пока сервер загружает данные и вызываетgetState()Получить состояние и вернуть его клиенту, а клиент может получить это состояние

существуетserver.jsПолучить начальное состояние изwindow.__INITIAL_STATE__сэкономить на клиенте

src/server.js

let preloadedState = {};
...

// resolve所有asyncData
Promise.all(promises).then(() => {
  // 获取预加载的state,供客户端初始化
  preloadedState = store.getState();
  // 异步数据请求完成后进行服务端render
  handleRender();
}).catch(error => {
  console.log(error);
  res.status(500).send("Internal server error");
});

...
let htmlStr = template
.replace(/<title>.*<\/title>/, `${head.title.toString()}`)
.replace("<!--react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()}
  <script type="text/javascript">
    window.__INITIAL_STATE__ = ${JSON.stringify(preloadedState)}
  </script>
`)
.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>\n${loadableState.getScriptTag()}`);

App.jsxполучено вwindow.__INITIAL_STATE__

// 获取服务端初始化的state,创建store
const initialState = window.__INITIAL_STATE__;
const store = createStore(initialState);

На этом этапе данные клиента и сервера могут быть синхронизированы.

Сбор данных клиента

Для маршрутизации на стороне клиента это делается в браузере, в это время клиенту также необходимо запросить данные.

существуетTopListкомпонентcomponentDidMountв функции жизненного циклаdispatchФункция создания асинхронного действияfatchTopListВозвращаемое значение

componentDidMount() {
  this.props.dispatch(fatchTopList());
}

Этот компонент уже создан, поэтому к нему можно получить доступ черезthisПосетите магазинdispatch, и эта функция будет выполняться только на стороне клиента

вы можете захотетьcomponentWillMountсерединаdispatchАсинхронное действие, чиновник изменил функцию жизненного цикла (пожалуйста, ткнитездесь), включено в версии 16.xcomponentWillMount,componentWillReceivePropsа такжеcomponentWillUpdateПредупреждение об истечении срока действия, эти три периодические функции будут удалены в версии 17. Рекомендуется использоватьcomponentDidMountПолучить данные от (пожалуйста, отметьтездесь)

Бывает ситуация, что если сервер загружает данные заранее, то когда клиент монтирует DOM, он выполняетcomponentDidMountВыполнит еще одну загрузку данных, на этот раз загрузка данных является избыточной, см. следующий рисунок.

доступhttp://localhost:3000/top-list, сервер выполнил предварительную выборку данных и обработал результирующую строку HTML.Красное поле — это запрос, отправленный после монтирования клиентского DOM. Чтобы избежать этого, добавьте новое состояние с именемclientShouldLoadПо умолчаниюtrue, указывающий, загружает ли клиент данные, т.е.clientShouldLoadНапишите actionType, функцию создания действия и функцию редуктора.

actionTypes.js

export const SET_CLIENT_LOAD = "SET_CLIENT_LOAD";

actions.js

import { SET_CLIENT_LOAD, SET_TOP_LIST, SET_TOP_DETAIL } from "./actionTypes";

export function setClientLoad(clientShouldLoad) {
  return { type: SET_CLIENT_LOAD, clientShouldLoad };
}

reducers.js

const initialState = {
  clientShouldLoad: true,
  topList: [],
  topDetail: {}
}

function clientShouldLoad(clientShouldLoad = initialState.clientShouldLoad, action) {
  switch (action.type) {
    case ActionTypes.SET_CLIENT_LOAD:
      return action.clientShouldLoad;
    default:
      return clientShouldLoad;
  }
}
...

const reducer = combineReducers({
  clientShouldLoad,
  topList,
  topDetail
});

компонент контейнераTopListсредняя параclientShouldLoadкарта

src/containers/TopList.jsx

const mapStateToProps = (state) => ({
    clientShouldLoad: state.clientShouldLoad,
    topList: state.topList
});

Изменить, когда сервер выполняет предварительную выборку данныхclientShouldLoadдляfalse, судить после монтирования клиентаclientShouldLoadЭтоtrue, еслиtrueполучить данные дляfalseбудуclientShouldLoadизменить наtrue, чтобы клиент мог получить его после перехода на другие маршрутыclientShouldLoadдляtrue, для сбора данных

В действии создайте асинхронную функцию, в настоящее время выполняются данные на стороне сервера, запрос завершен отправкой

actions.js

export function fatchTopList() {
  // dispatch由thunkMiddleware传入
  return (dispatch, getState) => {
    return getTopList().then(response => {
      const data = response.data;
      if (data.code === 0) {
        // 获取数据后dispatch,存入store
        dispatch(setTopList(data.data.topList));
      }
      if (process.env.REACT_ENV === "server") {
        dispatch(setClientLoad(false));
      }
    });
  }
}

TopListДобавьте оценку компонентам

TopList.jsx

componentDidMount() {
  // 判断是否需要加载数据
  if (this.props.clientShouldLoad === true) {
    this.props.dispatch(fatchTopList());
  } else {
    // 客户端执行后,将客户端是否加载数据设置为true
    this.props.dispatch(setClientLoad(true));
  }
}

посетить в это времяhttp://localhost:3000/top-list, у клиента на один запрос данных меньше. Как показано ниже

Суммировать

В этом разделе используется функция динамического импорта веб-пакета для ленивой загрузки маршрутов, чтобы уменьшить размер упакованных файлов и загружать их по запросу Плагин CommonsChunkPlugin, который поставляется с веб-пакетом, используется для разделения сторонних модулей, чтобы клиент мог лучше кэшировать . Общий клиент получает данные после монтирования DOM, а рендеринг на стороне сервера должен заранее загрузить данные на сервер, а затем вернуть данные клиенту, а клиент получает данные, возвращенные сервером, чтобы гарантировать что внешние и внутренние данные согласованы

Построение рендеринга на стороне сервера — очень утомительный и сложный процесс. Одной статьи недостаточно, чтобы представить основные моменты, необходимые для фактической разработки. участие Много очков. Официального полного набора кейсов для рендеринга на стороне сервера нет, так что подход не единственный

наконец

Рендеринг на стороне сервера включает в себя внутреннее поле.Помимо оптимизации на стороне клиента, в реальных проектах также требуется оптимизация на стороне сервера. Если вы используете рендеринг на стороне сервера в продакшене и вам нужно выполнять загрузку на стороне сервера при большом количестве пользователей, выберите разумную стратегию кэширования.

исходный код