Additional SSR performance with Nuxt fullstack server (Часть 2)

—

от автора

Additional SSR performance with Nuxt fullstack server

В Части 1 я рассказал как легко организовать API Server в Nuxt. В Части 2 я хочу рассказать какие дополнительные преимущества можно извлечь из Nuxt fullstack server.

Часть 2: ускоряем серверный рендериг!

Давайте подумаем сейчас как работает наш сервер из примера
codesandbox.io/s/codesandbox-nuxt-3gzhl.

  1. клиент запрашивает главную страницу
    3gzhl.sse.codesandbox.io
  2. Nuxt начинает рендерить на сервере страницу /pages/index.vue
  3. доходит до
      async fetch() {     this.users = await this.$api("users", "index");   }, 
  4. через axios делает http запрос, по адресу
    3gzhl.sse.codesandbox.io/api/users/index
    т.е. сам на себя
  5. устанавливается соединение, создаётся новая сессия на сервере и выделяется память для обработки http запроса
  6. принимается входящий запрос по протоколу http, парсится url, обрабатываются параметры
  7. отрабатывает серверное middleware
  8. Nuxt запускает наш API Server
  9. парсинг параметров в JSON
  10. вызывается искомый контроллер users.index(), который возвращает данные JSON
  11. JSON данные преобразовываются в строку и отправляются по протоколу http
  12. данные принимаются библиотекой axios и парсятся в JSON
  13. сессия API завершается

Теперь представим, что у нас на странице находится 20 компонентов которые запрашивают данные через API, таким образом за один запрос страницы c Nuxt сервером будет установлено 20 дополнительных внутренних http соединений и пункты 4-13 будут выполнены 20 раз. Nuxt HTTP сервер может обрабатывать более 55 тысяч запросов в секунду, однако создавая внутренние HTTP запросы, мы уменьшаем потенциальные ресурсы сервера в десятки раз.
Но ведь когда мы рендерим страницу на сервере, у нас есть прямой доступ до всех контроллеров в папке /api/

Давайте изменим логику таким образом, чтобы при рендеренге на сервере код контроллера вызывался напрямую, а при вызове из браузера запрос шёл по http

  1. Переименуем файл
    /plugins/api-context.js
    в
    /plugins/api-context.client.js
  2. изменим имя файла в настройках
    /nuxt.config.js
      plugins: ["~/plugins/api-context.client.js"] 

    Теперь контекст this.$api доступен только для клиентского кода

  3. создадим контекст this.$api для прямого вызова контроллеров на сервере
    /plugins/api-context.server.js
    export default (context, inject) => {   inject("api", async (controller, method, params) => {     try {       let api = require("../api/" + controller.replace(/^\/+|\/+$|\.+/g, ""));       return await api[method](params);     } catch (e) {       console.error(e);       throw e;     }   }); }; 
  4. подключим серверный плагин
    /nuxt.config.js
      plugins: [     "~/plugins/api-context.client.js",     "~/plugins/api-context.server.js"   ] 

Теперь функция this.$api на сервере будет напрямую вызывать метод контроллера, а на клиенте this.$api отправлять http запрос через axios.
код

  async fetch() {     this.users = await this.$api("users", "index");   }, 

при рендере на сервере, не будет выполнять http запрос на себя же, а просто подключит через require файл /api/users.js и вызовет метод index(), т.е. не будут выполняться пункты с 4-13, а выполнится только 10.
Однако когда клиент в браузере нажмёт кнопку Refresh, то те же самые данные запросятся через http

Вот полный код:
codesandbox.io/s/codesandbox-nuxt-pbriw

Тестирование производительности

codesandbox.io/s/codesandbox-nuxt-rzdyw

  1. Для исключения влияния скорости внешних соединений, я заменил получение данных, на статичные данные:
    /api/users.js
    // we can get data from any DB async function getDataFromDB() {   return {     page: 1,     per_page: 6,     total: 12,     total_pages: 2,     data: [       {         id: 1,         email: "george.bluth@reqres.in",         first_name: "George",         last_name: "Bluth",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg"       },       {         id: 2,         email: "janet.weaver@reqres.in",         first_name: "Janet",         last_name: "Weaver",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"       },       {         id: 3,         email: "emma.wong@reqres.in",         first_name: "Emma",         last_name: "Wong",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/olegpogodaev/128.jpg"       },       {         id: 4,         email: "eve.holt@reqres.in",         first_name: "Eve",         last_name: "Holt",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"       },       {         id: 5,         email: "charles.morris@reqres.in",         first_name: "Charles",         last_name: "Morris",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"       },       {         id: 6,         email: "tracey.ramos@reqres.in",         first_name: "Tracey",         last_name: "Ramos",         avatar:           "https://s3.amazonaws.com/uifaces/faces/twitter/bigmancho/128.jpg"       }     ],     ad: {       company: "StatusCode Weekly",       url: "http://statuscode.org/",       text:         "A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."     }   };   /*   return (await require("axios").get(`https://reqres.in/api/users?page=1`))     .data;   */ } .... 
  2. Изменим вызов api на сервере, добавим возможность получения данных принудительно через http и признак серверных данных
    /plugins/api-context.server.js
    export default (context, inject) => {   inject("server", () => true);   inject("api", async (controller, method, params) => {     try {       if (params && params.httpcall) {         return await context.$axios["$" + (params ? "post" : "get")](           "/api/" + controller + "/" + method,           params         );       }       let api = require("../api/" + controller.replace(/^\/+|\/+$|\.+/g, ""));       return await api[method](params);     } catch (e) {       console.error(e);       throw e;     }   }); }; 
  3. на странице index.vue в методе fetch асинхронно вызовем api 50 раз внутренним способом
    /pages/index.vue
      async fetch() {     let start = new Date();     let promises = [];     let callNum = 50;     for (let i = 0; i < callNum; i++) {       promises.push(this.$api("users", "index"));     }      let arr = await Promise.all(       promises.map(async p => {         return await p;       })     );      let res = [];     for (let r of arr) {       res = res.concat(r);     }      this.users = res;     this.fetchType =       (this.$server && this.$server() ? "Server internal" : "Client http") +       " API call";     this.fetchTime = new Date() - start;   }, 
  4. а на странице httpcall.vue в методе fetch асинхронно вызовем api 50 раз через http
    /pages/httpcall.vue
    ...       promises.push(this.$api("users", "index", { httpcall: true })); ...     this.fetchType =       (this.$server && this.$server() ? "Server http" : "Client http") +       " API call"; ... 
  5. теперь сравним время выполнения
    rzdyw.sse.codesandbox.io
    Server internal API call rendering fetch time: 1ms
    время от 0ms до максимум 2ms

    rzdyw.sse.codesandbox.io/httpcall
    Server http API call rendering fetch time: 71ms
    время от 46ms до максимум 1059ms
    и несколько раз сервер вообще падал с ошибкой
    RangeError
    Maximum call stack size exceeded

вот полный пример
codesandbox.io/s/codesandbox-nuxt-rzdyw

Итого Часть 2

  • минимальными изменениями можно ускорить серверный рендеринг более чем в 50 раз, на живом примере рендеринг страницы у меня ускорялся в ~1.7 раза
  • существенно сократился расход ресурсов Node HTTP сервера
  • в оптимизированном виде единственный инстанс Nuxt’а должен выдержать нагрузку небольших и средних проектов

ссылка на оригинал статьи https://habr.com/ru/post/501688/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *