Additional SSR performance with Nuxt fullstack server (Часть 2)
В Части 1 я рассказал как легко организовать API Server в Nuxt. В Части 2 я хочу рассказать какие дополнительные преимущества можно извлечь из Nuxt fullstack server.
Часть 2: ускоряем серверный рендериг!
Давайте подумаем сейчас как работает наш сервер из примера
codesandbox.io/s/codesandbox-nuxt-3gzhl.
- клиент запрашивает главную страницу
3gzhl.sse.codesandbox.io - Nuxt начинает рендерить на сервере страницу /pages/index.vue
- доходит до
async fetch() { this.users = await this.$api("users", "index"); },
- через axios делает http запрос, по адресу
3gzhl.sse.codesandbox.io/api/users/index
т.е. сам на себя - устанавливается соединение, создаётся новая сессия на сервере и выделяется память для обработки http запроса
- принимается входящий запрос по протоколу http, парсится url, обрабатываются параметры
- отрабатывает серверное middleware
- Nuxt запускает наш API Server
- парсинг параметров в JSON
- вызывается искомый контроллер users.index (), который возвращает данные JSON
- JSON данные преобразовываются в строку и отправляются по протоколу http
- данные принимаются библиотекой axios и парсятся в JSON
- сессия API завершается
Теперь представим, что у нас на странице находится 20 компонентов которые запрашивают данные через API, таким образом за один запрос страницы c Nuxt сервером будет установлено 20 дополнительных внутренних http соединений и пункты 4–13 будут выполнены 20 раз. Nuxt HTTP сервер может обрабатывать более 55 тысяч запросов в секунду, однако создавая внутренние HTTP запросы, мы уменьшаем потенциальные ресурсы сервера в десятки раз.
Но ведь когда мы рендерим страницу на сервере, у нас есть прямой доступ до всех контроллеров в папке /api/
Давайте изменим логику таким образом, чтобы при рендеренге на сервере код контроллера вызывался напрямую, а при вызове из браузера запрос шёл по http
- Переименуем файл
/plugins/api-context.js
в
/plugins/api-context.client.js - изменим имя файла в настройках
/nuxt.config.jsplugins: ["~/plugins/api-context.client.js"]
Теперь контекст this.$api доступен только для клиентского кода - создадим контекст this.$api для прямого вызова контроллеров на сервере
/plugins/api-context.server.jsexport 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; } }); };
- подключим серверный плагин
/nuxt.config.jsplugins: [ "~/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
- Для исключения влияния скорости внешних соединений, я заменил получение данных, на статичные данные:
/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; */ } ....
- Изменим вызов api на сервере, добавим возможность получения данных принудительно через http и признак серверных данных
/plugins/api-context.server.jsexport 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; } }); };
- на странице index.vue в методе fetch асинхронно вызовем api 50 раз внутренним способом
/pages/index.vueasync 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; },
- а на странице 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"; ...
- теперь сравним время выполнения
rzdyw.sse.codesandbox.io
Server internal API call rendering fetch time: 1ms
время от 0ms до максимум 2msrzdyw.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’а должен выдержать нагрузку небольших и средних проектов