Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як обслуговувати статичні файли та покращити продуктивність в Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`express.static()`** - middleware Express, який роздає HTML, CSS, JS та зображення з директорії без написання окремих роутів. ```js app.use(compression()); // обов'язково першим app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1y', etag: true, })); ``` Додай `compression()` перед ним для gzip-стиснення. Для файлів з перевіркою прав - окремий роут з `res.sendFile` після auth middleware. **Ключове:** `compression()` перед `express.static()`, не після.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`express.static()`** - вбудований middleware Express, який роздає файли HTML, CSS, JS та зображення прямо з директорії, без жодного окремого route-хендлера для кожного файлу. ## Теорія ### TL;DR - Уявіть шведський стіл: гості самі беруть їжу замість того, щоб чекати на кухню. Статичні файли йдуть з диску прямо в браузер, минаючи всю логіку роутів. - `express.static()` передає файли через `fs.createReadStream`, обходячи стек парсерів Express. - Заголовки ETag дозволяють браузерам пропускати повторне завантаження (304 Not Modified замість 200). - Головне правило: використовуй для CSS/JS/зображень, не використовуй для файлів користувачів без перевірки прав. - Найбільший виграш у продуктивності: постав `compression()` перед ним і встанови `maxAge` для кешування. ### Швидкий приклад ```js const express = require('express'); const path = require('path'); const app = express(); // Роздаємо з папки 'public' - /style.css відповідає public/style.css app.use(express.static(path.join(__dirname, 'public'))); app.listen(3000); // GET /style.css → 200 OK, роздає public/style.css // GET / → 200 OK, роздає public/index.html (якщо є) // GET /api/users → передає далі до наступного middleware (файл не знайдено) ``` Зверни увагу: `path.join(__dirname, 'public')` замість просто `'public'`. Ця різниця дає про себе знати на деплої. ### Як це працює всередині `express.static()` зчитує URL запиту, шукає відповідний файл і передає його через `fs.createReadStream`. Ключове слово тут - стрімінг (streaming). Байти йдуть з диску прямо у відповідь, без завантаження файлу в пам'ять. Саме тому відео на 50MB не кладе сервер. Для кешування middleware генерує ETag, хешуючи вміст файлу. При наступному запиті браузер надсилає `If-None-Match: "abc123"`. Express перевіряє, чи змінився файл. Якщо ні - відповідає кодом 304 і не надсилає жодного байта. Користувачі отримують швидке завантаження, ти економиш трафік. Динамічні роути на зразок `app.get('/api/data', handler)` виконують код на кожен запит. Статичний middleware для знайдених файлів взагалі не торкається твого коду. ### Коли використовувати - Публічні CSS/JS/зображення: `app.use(express.static('public'))` - Версіоновані ресурси з URL-префіксом: `app.use('/v1', express.static('dist'))` - SPA (React, Vue): роздаємо папку з білдом, потім перехоплюємо всі роути і відправляємо `index.html` - Файли з перевіркою прав: не використовуй `express.static` напряму. Постав auth middleware перед ним або роздавай через `res.sendFile` після перевірки. - Фронтенду немає зовсім: пропускай. Трохи менше часу на старт. ### Налаштування кешування та стиснення ```js const compression = require('compression'); // compression має бути ДО express.static app.use(compression()); app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1y', // кешувати на 1 рік (для хешованих імен файлів) etag: true, // заголовок ETag для перевірки кешу lastModified: true, // заголовок Last-Modified dotfiles: 'ignore', // ніколи не відкривати .env або .git })); ``` Чому `compression` перед `express.static`? Compression обгортає стрім відповіді. Якщо статика стоїть першою, байти вже відправлені до того, як compression встигне їх перехопити. Порядок middleware в Express важливий. Для білдів Create React App (файли мають хешовані імена на зразок `main.abc123.js`), `maxAge: '1y'` разом з `immutable: true` - правильне рішення. Хеш змінюється при кожному деплої, тому старий кеш стає недійсним автоматично. Для звичайних HTML-файлів - навпаки: без кешу або з коротким. ```js // Довгий кеш для хешованих статичних ресурсів app.use('/static', express.static('public', { maxAge: '1y', setHeaders: (res, filePath) => { if (filePath.endsWith('.js')) { res.set('Cache-Control', 'public, immutable'); } } })); // Без кешу для HTML app.use((req, res, next) => { if (req.path.endsWith('.html')) { res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); } next(); }); ``` ### Типові помилки **Помилка 1: Статика поставлена до API-роутів** ```js // Неправильно - може перехопити /api/users, якщо файл існує app.use(express.static('public')); app.get('/api/users', handler); // ніколи не виконається в такому разі // Правильно app.get('/api/users', handler); app.use(express.static('public')); ``` Статичний middleware першим знаходить збіг і забирає запит. Якщо файл `public/api/users` існує, твій хендлер не запуститься. **Помилка 2: Відносний шлях без `__dirname`** ```js // Неправильно - працює в dev, ламається на деплої app.use(express.static('public')); // Правильно - завжди резолвиться від директорії скрипта app.use(express.static(path.join(__dirname, 'public'))); ``` Просто `'public'` резолвиться від `process.cwd()`, який залежить від того, де ти запускаєш процес. `__dirname` - це завжди директорія файлу з кодом. **Помилка 3: Немає `maxAge` в продакшені** ```js // Неправильно - кожен користувач перезавантажує 2MB JS-бандл при кожному візиті app.use(express.static('public')); // Правильно app.use(express.static('public', { maxAge: '1y' })); // для хешованих файлів ``` **Помилка 4: Роздача завантажень користувачів як статики** ```js // Неправильно - будь-хто бачить будь-який завантажений файл app.use('/uploads', express.static('uploads')); // Правильно - спочатку перевіряємо авторизацію app.get('/uploads/:id', authMiddleware, (req, res) => { res.sendFile(path.join(__dirname, 'uploads', req.params.id)); }); ``` Це одна з найпоширеніших дірок у безпеці Express-додатків. Без перевірки прав кожен файл у папці стає публічним. ### Де зустрічається в реальних проектах - Create React App: `app.use(express.static(path.join(__dirname, 'build')))` - стандартне налаштування для продакшену. - Next.js standalone: власний статичний хендлер з immutable-кешуванням для `/_next/static`. - Express + Socket.io: спочатку статика, потім монтування socket-хендлера, щоб не було конфліктів URL. - До 10k запитів на секунду: Express static достатньо. Більше - ставь nginx або CDN (CloudFront, Cloudflare) перед додатком. На низькотрафікових внутрішніх інструментах я бачив команди, які взагалі не підключали nginx. Воно працює. Але в продакшені з реальними користувачами nginx або CDN економлять і гроші, і час відповіді. ### Follow-up питання **Q:** Яка різниця в продуктивності між `express.static` і власним хендлером через `fs.readFile`? **A:** Static використовує `fs.createReadStream`, який передає дані у відповідь без буферизації. `fs.readFile` завантажує весь файл у пам'ять, що призводить до аварій на великих файлах і збільшення RAM при навантаженні. **Q:** Як працює ETag у процесі перевірки кешу? **A:** Сервер надсилає `ETag: "abc123"` (MD5-хеш файлу). Браузер зберігає його і при наступному запиті надсилає `If-None-Match: "abc123"`. Express перевіряє, чи збігається хеш, і якщо так - відповідає 304 без тіла відповіді. **Q:** Чому `compression()` має стояти перед `express.static()`? **A:** Compression обгортає вихідний стрім відповіді. Express.static генерує цей стрім. Якщо static запускається першим, байти вже відправлені до того, як compression може їх перехопити. **Q:** Як інвалідувати кеш браузера після деплою? **A:** Використовуй хешування імен файлів. Webpack і Vite додають хеш вмісту до імені (наприклад, `main.abc123.js`). Ти встановлюєш `maxAge: '1y'`, і коли файл змінюється, його ім'я теж змінюється. Старий кеш стає недійсним автоматично. **Q:** Як підключити SPA-роутинг (React Router) разом з express.static? **A:** Спочатку роздаємо папку з білдом, потім перехоплюємо всі решта роути і надсилаємо `index.html`. Client-side роутер обробляє все інше. ```js app.use(express.static(path.join(__dirname, 'build'))); app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'build/index.html'))); ``` ## Приклади ### Базовий: роздача публічної папки з кешуванням ```js const express = require('express'); const path = require('path'); const app = express(); app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h', // браузер кешує на 1 годину })); app.listen(3000); // Перший GET /logo.png → 200, Cache-Control: public, max-age=3600 // Другий GET /logo.png → 304 (з кешу браузера, 0 байт надіслано) ``` Файли в `public/` відображаються прямо на URL. `public/logo.png` стає `/logo.png`, `public/js/app.js` стає `/js/app.js`. Жодного коду роутів не потрібно. ### Середній рівень: продакшен-білд React зі стисненням ```js const express = require('express'); const path = require('path'); const compression = require('compression'); const app = express(); // Gzip для відповідей більше 1kb app.use(compression()); // Роздаємо React-білд - хешовані імена дозволяють кешувати на рік app.use(express.static(path.join(__dirname, 'build'), { maxAge: '1y', etag: true, lastModified: true, })); // Fallback для SPA - React Router обробляє навігацію на клієнті app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'build/index.html')); }); app.listen(3000); // /static/js/main.abc123.js → 200 gzip (45kb → 12kb), max-age=31536000 // Перезавантаження сторінки → 304, 0 байт завантажено ``` Стиснення зменшує типовий JS-бандл з 45kb приблизно до 12kb. У поєднанні з річним кешем повторні відвідувачі отримують майже миттєве завантаження. ### Просунутий рівень: різні заголовки кешу для різних типів файлів ```js const express = require('express'); const path = require('path'); const compression = require('compression'); const app = express(); app.use(compression()); app.use('/static', express.static(path.join(__dirname, 'public'), { maxAge: '1y', setHeaders: (res, filePath) => { // Хешовані JS/CSS: immutable, кешувати назавжди if (filePath.match(/\.(js|css)$/)) { res.set('Cache-Control', 'public, max-age=31536000, immutable'); } // Зображення: кешувати на 30 днів if (filePath.match(/\.(png|jpg|svg|webp)$/)) { res.set('Cache-Control', 'public, max-age=2592000'); } } })); // API-роути не зачіпаються app.get('/api/users', (req, res) => { res.json({ users: [] }); }); app.listen(3000); ``` `setHeaders` дає контроль над кожним типом файлу без окремих роутів. Хешовані файли отримують `immutable`, і браузер більше не перевіряє їх зовсім. Зображення кешуються коротше, бо можуть змінитися без зміни імені файлу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.