---
title: "Производительность Fasthttp"
description: "Урок fasthttp производительность: zero allocation и пулы объектов, fiber benchmark и pprof по шагам, fiber vs gin и границы применимости быстрых путей."
course: "Fiber: экспресс-подобный веб-фреймворк на Fasthttp"
url: "https://golangmaster.ru/courses/fiber/lessons/24-proizvoditelnost/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Производительность Fasthttp

*Курс «Fiber: экспресс-подобный веб-фреймворк на Fasthttp», урок 24 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fiber/lessons/24-proizvoditelnost/*

Прошлый урок закрылся обещанием: сегодня мы поставили приложение на боевой сервер, завтра заглянем под капот движка, который этот сервер кормит. Исполняем. Тема «fasthttp производительность» — это разбор сделки, которую движок заключил за вас: zero memory allocations на горячем пути в обмен на совместимость с net/http. Мы разберём, где живут fiber аллокации и почему методы контекста отдают значения с коротким сроком годности, посчитаем всё руками: соберём fiber benchmark командой go test -bench с b.ReportAllocs, вскроем профиль pprof через отдельный net/http-порт, покрутим GOGC и GOMEMLIMIT — и честно проговорим границы применимости, на которых быстрые пути fasthttp перестают помогать. Сравнение fiber vs gin получит числа и критерии вместо лозунгов: фундамент определяет совместимость, а профиль нагрузки — выбор. Опоры уже стоят: [конфигурационный урок двадцатый](/courses/fiber/lessons/20-konfiguraciya/) назвал поля Concurrency и ReadBufferSize, [деплой двадцать третий](/courses/fiber/lessons/23-deploy/) дал боевой контекст — контейнер с лимитами CPU, nginx впереди. Все факты сверены с исходниками fiber v3.5.0 и его движком fasthttp v1.73.0 из go.mod.

### Мост из прошлого урока: под капот движка, который кормит сервер

Начнём почти дословно с того, чем закончили: «двадцать четвёртый урок — производительность Fasthttp: почему Fiber быстр, zero allocation и границы применимости. Логика прямая: сегодня мы поставили приложение на боевой сервер, завтра заглянем под капот движка, который этот сервер кормит — и поймём, за счёт чего fiber и выигрывает те самые бенчмарки, и на каких нагрузках быстрые пути fasthttp перестают помогать». Обещание исполняется в четыре захода. Первый — механика: что именно fasthttp переиспользует и почему на горячем пути не возникает аллокаций, которых не было бы у net/http. Второй — цена: значения, которые вы вытаскиваете из fiber.Ctx, живут ровно до возврата хендлера, и это следствие той же механики, а не каприз документации. Третий — измерение: fiber benchmark, который вы соберёте сами, и pprof, который покажет, куда уходит время и память вашего кода, а не движка. Четвёртый — границы: CRUD с походом в базу, стриминг больших тел и HTTP/2 — случаи, где смена движка не сдвинет ни миллисекунду. Канон курса остаётся при нас: инстанс на :8080, маршрут /api/tasks, конверт items, хендлер с c.JSON — теперь мы измерим и его тоже.

### fasthttp производительность: сделка с переиспользованием

Прочитаем шапку fasthttp так, как её написал автор, дословно: «Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http». Три обещания, и первое — technical, а не рекламное: zero memory allocations in hot paths. Горячий путь — это код, который выполняется на каждом запросе: распарсить заголовки, найти маршрут, подготовить контекст, собрать ответ. У стандартного net/http каждый такой запрос рождает новые объекты: http.Request, http.ResponseWriter, буферы чтения — сборщик мусора честно прибирает всё это, но тратит на прибирание процессорное время. Fasthttp пошёл другим путём: объекты контекста, буферы заголовков и тел не создаются на запрос, а берутся из пула и после ответа моются и возвращаются туда же. Десяток раз быстрее — это на синтетике с мелкими ответами и keep-alive-соединениями, и к честности цифр мы вернёмся в блоке про fiber benchmark; важно другое: выигрыш рождается не из магии, а из отказа создавать то, что можно переиспользовать. У всякой сделки есть цена, и здесь она видна в названии пакета: fasthttp — это не net/http. Типы несовместимы, готовые http.Handler-middleware не подключаются напрямую, а HTTP/2 движок не поддерживает вовсе. За скорость платят экосистемой, и умный выбор требует понимать обе стороны весов — этому посвящён блок fiber vs gin.

### Канон: хендлер под микроскопом аллокаций

```go
app.Get("/api/tasks", func(c fiber.Ctx) error {
    // быстрый путь: движок не аллоцировал ни байта до этой строки
    // svc — сервис из композиционного корня двадцать второго урока
    items, err := svc.List(c.Context())
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "сервис недоступен")
    }
    // а вот здесь аллокации есть: encoding/json строит байты ответа
    return c.JSON(fiber.Map{"items": items})
})
```

Разложим канон курса по строке аллокаций, потому что здесь спрятана самая частая ошибка чтения лозунгов. Всё, что сделал движок до вашей лямбды, — принял соединение, разобрал заголовки, нашёл маршрут, выдал контекст из пула — действительно прошло без аллокаций: это и есть обещание zero allocation. Но внутри хендлера мир меняется: svc.List отдаёт слайс структур — аллокация, вызванная вашим кодом; c.JSON вызывает кодировщик, а дефолтный JSONEncoder в fiber.Config — это json.Marshal, и маршалинг заведомо строит новые байты, плюс сама карта fiber.Map — аллокация. Выражение «движок без аллокаций» описывает накладные расходы фреймворка, а не устраняет ваши. Практический вывод двоякий. Первый: профиль вашего приложения почти всегда будет во главе списка не fiber, а ваш json.Marshal и ваши походы в хранилище — это нормально и именно это покажет pprof ниже. Второй: апгрейд фреймворка не отменяет дорогой хендлер — оптимизировать надо то, что профиль назвал главным, а не то, что маркетинг назвал быстрым. Запомните этот хендлер: он станет героем benchmark-блока.

### fiber аллокации: UnsafeString и правило одной жизни значения

Теперь цена zero allocation, и она записана в исходниках fiber v3.5.0 прямо. При старте приложения поле toString получает значение utils.UnsafeString — это конверсия байтов в строку без копии, через unsafe: строка, которую вам вернул метод контекста, не владеет своей памятью, она указывает в буфер соединения, который движок потом использует заново. Док-комментарий метода Params в req.go формулирует контракт дословно: «Returned value is only valid within the handler. Do not store any references. Make copies or use the Immutable setting to use the value outside the Handler» — значение живёт только внутри хендлера, ссылок не хранить, нужен хранить — копируйте или включайте Immutable. Комментарий к методу, возвращающему метод запроса, объясняет, что бывает с нарушителями: «an unsafe alias into the request buffer would be silently corrupted on connection reuse» — алиас в буфер соединения тихо портится при переиспользовании соединения. Тихо — ключевое слово: не паника, не гонка с детектором, а мусорные байты в чужом ответе, который вы будете ловить по дням. Отсюда и выход из сделки — поле fiber.Config.Immutable: его док-комментарий признаётся, что при true фреймворк «relinquishes the 0-allocation promise» — отказывается от обещания нуля аллокаций в отдельных случаях, — зато все значения контекста становятся честными копиями и переживают возврат из хендлера. fiber аллокации — это не враг, а параметр сделки: по умолчанию вы получаете скорость и отвечаете за дисциплину; Immutable — скорость чуть ниже, спокойный сон выше.

### Проверьте себя: значение, пережившее хендлер

**Вопрос:** Хендлер передаёт заголовок запроса в фоновую горутину. Что произойдёт при наплыве клиентов?

- ✅ h — строка без копии, алиас в буфер соединения: соединение вернётся в пул, байты затрутся следующим запросом, и журнал получит мусор — «silently corrupted on connection reuse»
- Ничего плохого: Go при передаче в горутину всегда копирует строки
- Программа упадёт: fasthttp запрещает запускать горутины из хендлеров
- Компилятор не даст собрать код: нельзя использовать c.Get вне хендлера

Строка в Go — неизменяемая, но её данные лежат там, куда указывает указатель, и передаются в горутину без копии: копируется заголовок строки, а не байты. Дефолтный toString — utils.UnsafeString, то есть без копии вовсе: h указывает в переиспользуемый буфер заголовков. Соединение вернулось в пул, следующий клиент записал туда свои заголовки — горутина прочтёт чужие байты. Детектор гонок при этом может промолчать: он ловит только совпадающие во времени обращения, а движок перезаписывает буфер в собственные моменты жизни соединения — гонка реальна и без детектора. Лечится копией до выхода из хендлера: h := string(c.Get(...)) — да, это аллокация, и это честная цена; либо полем Immutable: true, если таких мест много. Запретов на горутины нет — запрет на ссылки, пережившие хендлер.

### fiber vs gin: фундамент решает

Первый урок курса положил сравнение на стол — теперь, под занавес, у нас достаточно словаря, чтобы сравнить по-взрослому, потому что выбор между движками — это fiber производительность против совместимости, и никак иначе. Gin стоит на net/http, а значит бесплатно получает всё, что стандартная библиотека накопила за пятнадцать лет: HTTP/2 и HTTPS из коробки, огромный парк готовых middleware в стиле http.Handler, httptest и httputil, профилировщик net/http/pprof одной строкой импорта, совместимость с прокси и service mesh, которые говорят по-настоящему современному HTTP. Fiber стоит на fasthttp: HTTP/2 движок не поддерживает, http.Handler-middleware не подключается напрямую — только через adaptor, и каждый мост между мирами стоит аллокаций, ровно тех, от которых мы уходили. В прикладном CRUD-сервисе разницы в скорости вы не увидите: узкое место — база и сериализация, о чём первый урок уже честно предупреждал. Правило выбора из практики: legacy-проект с http.Handler-наследством, потребность в gRPC-gateway, HTTP/2 или командной привычке к net/http — gin, и это не поражение fiber, а трезвый расчёт; новый JSON-API с высокой частотой мелких запросов, где профиль показал, что накладные расходы фреймворка реальны, — fiber оправдан. Синтетические бенчмарки, в которых fasthttp уходит вперёд в разы, описывают плечо «plaintext, крошечный ответ, keep-alive» — убедитесь, что ваше плечо именно такое, прежде чем делать вывод.

### fiber vs gin: сравнение по пунктам

| Критерий | Fiber (fasthttp) | Gin (net/http) |
| --- | --- | --- |
| Движок | fasthttp v1.73.0, собственный стек соединений | net/http — стандартная библиотека |
| Совместимость net/http | нет: типы несовместимы, мост — adaptor | полная: http.Handler, httptest, pprof |
| HTTP/2 | не поддерживается движком | есть из коробки, включая h2 через TLS |
| Аллокации горячего пути | пулы объектов, zero allocation по умолчанию | аллокации на каждый запрос, GC их собирает |
| Готовые middleware | свой комплект ядра и contrib | огромная экосистема http.Handler |
| Типичный выигрыш | мелкие ответы, keep-alive, высокие RPS | не в скорости, а в совместимости и HTTP/2 |

### Проверьте себя: выбор под нагрузкой

**Вопрос:** Проект: существующий сервис на net/http, нужен HTTP/2 и переиспользование пары десятков готовых http.Handler-middleware, а нагрузка — пара тысяч запросов в секунду CRUD-профиля. Какой фреймворк выбрать и почему?

- ✅ Gin: он на net/http, HTTP/2 и http.Handler-middleware работают без adaptor, а на CRUD-нагрузке узкое место — база, и сменой движка её не вылечить
- Fiber: он быстрее на бенчмарках, а бенчмарки всегда описывают ваш прод
- Никакой: пару тысяч RPS тянет только переписывание на сырой fasthttp
- Fiber, но с adaptor для старых middleware: мост бесплатный

Ключ вопроса — профиль нагрузки: CRUD с походом в базу на паре тысяч RPS живёт и умирает в хранилище и сериализации, где накладные расходы любого фреймворка — фон. Зато совместимость здесь — жёсткое требование: HTTP/2 в fasthttp нет вовсе, а adaptor между мирами — не бесплатный мост, он конвертирует типы и плодит ровно те аллокации, от которых fasthttp берегут. Вариант «бенчмарки всегда про ваш прод» — ловушка: синтетика меряет plaintext с крошечными ответами. Вариант «только сырой fasthttp» переоценивает сложность: пара тысяч RPS — комфортная зона любого фреймворка. Итог — gin, и fiber в этом сценарии был бы осознанным вредительством.

### fiber benchmark: что и как измерять

Синтетику вроде TechEmpower, где fasthttp-стеки штурмуют верх plaintext-таблицы, принимайте как описание верхнего потолка, а не вашего сервиса: там нет базы, нет бизнес-логики, а тело ответа — несколько байт. Ваш fiber benchmark должен мерить ваш хендлер на вашей машине, и у Go для этого есть штатный инструмент — тесты с b.N: [тестовый урок двадцать первый](/courses/fiber/lessons/21-testirovanie/) подарил нам app.Test, который гоняет запрос через полный стек без сети, — идеально для замера, потому что убирает TCP-шум и оставляет ровно то, что вы хотите сравнить. Команда запуска — go test -bench=. -benchmem -benchtime=2s ./internal/handlers/: флаг -benchmem добавит колонки памяти, -benchtime удлинит прогон, чтобы цифры устоялись. Сравнение версий делает benchstat: он берёт два файла вывода, считает разброс и честно говорит, значима ли разница, — без него «стало быстрее на пять процентов» почти всегда шум. И последнее правило гигиены замера: гоняйте бенчмарки без соседей по CPU — контейнер с лимитами из [деплой-урока двадцать третьего](/courses/fiber/lessons/23-deploy/) на перегруженном хосте даст разброс, при котором вы оптимизируете погоду.

### Канон: benchmark хендлера через app.Test

```go
// handlers_bench_test.go — замер канонного хендлера списка
package handlers

import (
        "net/http/httptest"
        "testing"

        "github.com/gofiber/fiber/v3"
        "myapi/internal/config"
        "myapi/internal/handlers"
        "myapi/internal/repository"
        "myapi/internal/service"
)

func BenchmarkListTasks(b *testing.B) {
        // сборка канона двадцать второго: память → сервис → хендлер → приложение
        repo := repository.NewMemoryRepo()
        h := handlers.NewTaskHandler(service.NewTasks(repo))
        app := buildApp(h, config.FromEnv()) // вне цикла b.N: сборка — не предмет замера

        b.ReportAllocs() // колонка allocs/op — главный улика
        b.ResetTimer()

        for i := 0; i < b.N; i++ {
                req := httptest.NewRequest("GET", "/api/tasks", nil)
                resp, err := app.Test(req)
                if err != nil {
                        b.Fatalf("app.Test: %v", err)
                }
                if resp.StatusCode != fiber.StatusOK {
                        b.Fatalf("статус: %d", resp.StatusCode)
                }
        }
}
```

Разбор по строкам, потому что в бенчмарке каждая — деньги. Сборка канона двадцать второго урока — память → сервис → хендлер → приложение — выполняется один раз до цикла: buildApp из двадцать первого урока возвращает готовый инстанс без прослушивания порта, конфиг — FromEnv, как в main. b.ReportAllocs() добавляет в вывод колонку allocs/op даже без флага -benchmem — приучитесь класть её в код, а не в память командной строки. b.ResetTimer() стоит после подготовки, чтобы разводка приложения не попала в замер; b.N Go подберёт сам, раскатывая цикл до устойчивых цифр. Внутри — тот же app.Test, что и в юнит-тестах: запрос строится httptest.NewRequest и проходит полный стек, поэтому замер честно включает middleware и сериализацию. Читаем вывод так: строка BenchmarkListTasks-8 — восьмиядерная машина; ns/op — время на одну итерацию; B/op — байты, выделенные на итерацию; allocs/op — число аллокаций. Наш канонный хендлер отдаст единицы аллокаций: карта конверта, маршалинг, слайс задач — всё своё; движок в этой колонке почти не участвует. Сравнивать версии стоит benchstat-ом: benchstat old.txt new.txt — и только значимая разница считается разницей.

### Прочитайте строку бенчмарка

```go
$ go test -bench=. -benchmem ./internal/handlers/
goos: linux
goarch: amd64
BenchmarkListTasks-8    1000000    1035 ns/op    512 B/op    8 allocs/op
```

**Что выведет программа?**

- ✅ 8: колонка allocs/op — число аллокаций на одну итерацию цикла b.N
- 512: это и есть аллокации, B/op — их счётчик
- 1000000: столько аллокаций сделал весь прогон
- 1035: аллокации в наносекундах — так Go их кодирует

Формат вывода testing фиксирован: имя-ядра, итерации b.N, наносекунды на итерацию, байты на итерацию, аллокации на итерацию. allocs/op — последняя колонка, восемь: именно её вы будете уменьшать, когда оптимизация станет задачей. 512 B/op — это объём, а не количество; 1000000 — сколько итераций прогнал тест, а не аллокаций; 1035 ns/op — время. Пара колонок B/op и allocs/op различает «много мелких» и «мало жирных»: сорок аллокаций по восемь байт лечатся иначе, чем одна на триста байт, и оба случая вы увидите по-разному в pprof — alloc_objects против alloc_space.

### pprof: куда утекают миллисекунды и байты

Бенчмарк отвечает «сколько», pprof — «куда». Каноническая пара — CPU-профиль и профиль памяти, и в Go оба собирает пакет runtime/pprof, а готовые эндпоинты для них регистрирует net/http/pprof на DefaultServeMux. Здесь таится первая ловушка fiber-мира: ваш процесс — не net/http-сервер, и /debug/pprof сам по себе на :8080 не появится. Решение каноническое и признанное: поднять рядом крошечный net/http-сервер на отдельном локальном порту, где DefaultServeMux отдаст профили. Это не костыль, а гигиена: профилировщик не должен светиться наружу и грузить боевой стек — localhost:6060 слушает только свои, за [nginx из деплой-урока](/courses/fiber/lessons/23-deploy/) его не выносят. Дальше — два рабочих приёма. CPU-профиль под реальной нагрузкой: go tool pprof собирает тридцать секунд выборки и показывает top, где flat — время собственной функции, cum — её же плюс вызванные. Профиль аллокаций: -sample_index=alloc_objects считает количество объектов, alloc_space — байты; первый находит «много мелких», второй — «мало жирных». flame graph (команда web или web в интерактивном pprof) покажет ширину стеков наглядно. И важная дисциплина: профилируйте под нагрузкой, похожей на боевую — профиль на пустом сервере честно покажет, что простаивание бесплатно.

### Канон: pprof-сервер рядом с fiber

```go
// cmd/api/main.go — профилировщик на соседнем порту, только localhost
import (
        "net/http"
        _ "net/http/pprof" // side-effect: регистрирует /debug/pprof на DefaultServeMux
)

func main() {
        // fiber на :8080 — боевой стек; pprof на :6060 — свой канал
        go func() {
                log.Println(http.ListenAndServe("localhost:6060", nil))
        }()

        app := buildApp(config.FromEnv())
        log.Fatal(app.Listen(":8080"))
}
```

Три строки, и каждая важна. Импорт с подчёркиванием — пакеты с side-effect-инициализацией в Go подключают именно так: net/http/pprof в init() вешает на DefaultServeMux хендлеры /debug/pprof/profile, /debug/pprof/heap, /debug/pprof/allocs и десяток соседей; сам пакет в коде не упоминается. http.ListenAndServe("localhost:6060", nil) — nil означает DefaultServeMux, тот самый, куда записался pprof; префикс localhost делает порт недоступным извне — профилировщик на боевом IP это дыра в периметре. Горутина с go — потому что main занят fiber- Listen: два сервера в одном процессе, у каждого свой порт и свой mux. Дальше команды: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 — тридцать секунд CPU-выборки под нагрузкой; go tool pprof -sample_index=alloc_objects http://localhost:6060/debug/pprof/heap — счётчик объектов; внутри — top, list по интересной функции (строчки с ценой прямо в исходнике), web для flame graph. Сняли профиль — сервер профилировщика можно и погасить: постоянный pprof в проде не нужен.

### Соберите pprof-обвязку и команду замера

Вставьте пропуски так, чтобы профилировщик поднялся на соседнем порту, а бенчмарк считал память и аллокации.

```go
import (
        "net/http"
        _ "net/http/___" // регистрирует хендлеры /debug/pprof
)

func main() {
        go func() {
                log.Println(http.___("localhost:6060", nil))
        }()
        // ...
}

// замер хендлеров с колонками памяти и аллокаций
$ go test -___=. -___mem -benchtime=2s ./internal/handlers/
```

Подсказка: Импорт пакета с side-effect-инициализацией заканчивается на pprof; DefaultServeMux обслуживает ListenAndServe с nil-хендлером; флаг тестов включает бенчмарки, флаг памяти — его суффикс.

Правильные ответы в пропусках: `pprof`, `ListenAndServe`, `bench`, `bench`

### GC под прицелом: GOGC, GOMEMLIMIT и ReduceMemoryUsage

Аллокации не бесплатны даже при нуле кадров: чем чаще выделение, тем чаще сборщик мусора выходит на работу, а его смены видны как хвосты задержек. Диагностика начинается с честного счётчика: runtime.ReadMemStats отдаёт NumGC, PauseTotalNs и HeapAlloc — нарисуйте их на графике рядом с RPS, и картина сборки станет видимой; GODEBUG=gctrace=1 печатает в stderr каждую сборку с её цифрами. Регулировок две, и они не равнозначны. GOGC задаёт, во сколько раз куча должна вырасти от последней сборки до следующей: дефолт 100 — сборка при удвоении; поднять — реже собирать и больше есть памяти, опустить — чаще и прожорливее по CPU. GOMEMLIMIT из Go 1.19 — мягкий потолок памяти процесса: GC старается не выходить за лимит, и это спасает контейнер с жёстким лимитом из деплой-урока от смерти по OOM; GOMEMLIMIT=GOGC, GOMEMLIMIT не заменяет, а ограничивает. У fiber есть и собственная ручка — поле ReduceMemoryUsage в конфиге, и его док-комментарий честен: «Aggressively reduces memory usage at the cost of higher CPU usage» — снижает память агрессивно, платя процессором, — и рекомендует включать, только когда сервер ест память на массе простаивающих keep-alive-соединений. Общее правило всех этих ручек одно: сначала уберите аллокации, которые профилирование назвало лишними, — крутить GC вместо исправления кода это поливать огонь бензином с соседнего ведра.

### Читаем pprof top

```go
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
(pprof) top5
Showing nodes accounting for 2.55s, 85.00% of 3s total
      flat  flat%   sum%        cum   cum%
     2.05s 68.33% 68.33%  2.15s 71.67%  encoding/json.Marshal
     0.20s  6.67% 75.00%  0.25s  8.33%  reflect.Value.Set
     0.15s  5.00% 80.00%  0.30s 10.00%  fasthttp.(*Server).serveConn
     0.10s  3.33% 83.33%  0.12s  4.00%  fiber.(*App).next
     0.05s  1.67% 85.00%  0.06s  2.00%  regexp.Match
```

**Что выведет программа?**

- ✅ encoding/json.Marshal: 68% flat — сначала оптимизировать сериализацию, а не фреймворк
- fasthttp.serveConn: движок съедает две трети — пора переезжать на gin
- fiber.next: middleware из шестого урока — снести их и станет быстро
- regexp.Match: заменить пакет regexp ручным парсером — всё остальное мелочь

Читаем колонку flat — собственное время функции: json.Marshal забрал 68%, все слои fiber и fasthttp вместе — около десятой части. Значит, и оптимизация, и упрёки адресованы сериализации: перегруженный DTO, лишние поля без omitempty, карта там, где хватило бы структуры, — приёмы из девятого урока. Переезд на gin менял бы только слои движка — те самые 10% cum у serveConn, — а две трети уехали бы вместе с encoding/json, потому что gin маршилирует тем же пакетом. Сносить middleware из-за 4% — спилить сук под собой: логи и recovery стоят своих процентов. regexp с двумя процентами правится последним, если вообще: правило pprof — сначала большой flat, потом cum, и только потом рефлексия про фреймворк.

### Границы применимости: где быстрые пути перестают помогать

Честный финал разбора — карта мест, где fasthttp не даст обещанного. Первая граница — профиль нагрузки: если время запроса — это база и внешний API, накладные расходы фреймворка — проценты от общего, и никакой движок их не спасёт; fiber производительность выигрывает там, где плечо — мелкие ответы и много keep-alive-соединений: health-чеки, plaintext, метрики, лёгкий JSON-конверт. Вторая граница — большие тела: по умолчанию движок буферизует весь запрос в памяти, BodyLimit по умолчанию 4 MiB, а при больших загрузках это и память, и задержка; поле StreamRequestBody в fiber.Config включает стриминг — док-комментарий говорит, что хендлер вызывается раньше, когда тело больше текущего лимита, — но стриминг вынуждает вас самим следить за тем, что вы читаете. Соседняя ручка DisablePreParseMultipartForm: движок по умолчанию сам парсит multipart-форму — для огромных файлов это лишняя работа, и её выключают, чтобы разбирать форму вручную и лениво, как делал одиннадцатый урок с загрузками. Третья граница — протоколы: HTTP/2 не поддерживается, а значит webpush-приложения за современным CDN и gRPC-овер-HTTP/2 мимо; WebSocket живёт через hijack, как в шестнадцатом уроке, — это решено, но это исключение, а не правило. Четвёртая — экосистема: каждый http.Handler-мост через adaptor съедает часть выигрыша. Ни одна из границ не приговор — это параметры сделки; плохо только не знать о них до выбора.

### Проверьте себя: ускорит ли переезд

**Вопрос:** Профиль fiber-сервиса: 68% — encoding/json.Marshal, 10% — движок, остальное — база. Команда предлагает переехать на gin «потому что fiber тормозит». Что скажет расчёт?

- ✅ Переезд не даст почти ничего: две трети времени — encoding/json, который gin использует тот же; 10% движка — потолок потенциального выигрыша, и то до вычета adaptor-ов
- Переезд даст 68% ускорения: весь flat json.Marshal исчезнет вместе с fiber
- Переезд даст ускорение на все 10% движка и ещё поднимет RPS базы
- Расчёт невозможен: профили не сравнивают фреймворки

Арифметика профиля проста: миграция меняет только то, что уходит с заменой — слои движка, около десятой части времени. encoding/json у gin тот же самый, база та же, DTO те же — 68% переезжают с вами в новый фреймворк без единой сэкономленной микросекунды, а если старые http.Handler-мосты понадобятся adaptor-ом, к расходам добавится конвертация типов. Правильная последовательность другая: оптимизировать сериализацию (структура вместо карты, omitempty, кодировщик в fiber.Config), замерить benchstat-ом, и только если после этого слои движка всё ещё значимы — говорить о смене фреймворка. Профили не только сравнивают фреймворки — они это делают лучше любых бенчмарков, лишь бы сняты были честно.

> **Преждевременная оптимизация — это не про скорость, а про порядок.** Порядок работ всегда один: замер без оптимизаций → pprof под реальной нагрузкой → оптимизация самого жирного пункта → замер после → benchstat. Перепрыгивание первого шага делает бессмысленными все остальные: без baseline нечему быть «после», а без боевого профиля вы оптимизируете нагрузку с ноутбука, которой не существует. И помните про пару тысяч RPS: в этой зоне фреймворк выбирают по удобству API и экосистеме — fiber в том числе, но не ради наносекунд.

### Чек-лист производительности fiber

- Baseline до всяких правок: go test -bench=. -benchmem -benchtime=2s и сохранённый вывод для benchstat.
- b.ReportAllocs() в каждом benchmark-е — колонка allocs/op дороже флага в командной строке.
- pprof на отдельном localhost-порту рядом с fiber; CPU-профиль 30 секунд под боеподобной нагрузкой.
- Читаем flat раньше cum: оптимизировать то, что во главе списка, а не то, что раздражает.
- Ни одной ссылки на значения fiber.Ctx за пределами хендлера: копия string(...) или Immutable: true.
- Большие тела: StreamRequestBody и DisablePreParseMultipartForm вместо слепого увеличения BodyLimit.
- Память контейнера: GOMEMLIMIT под лимит из деплой-урока, gctrace=1 при подозрениях, ReduceMemoryUsage — только при толпах idle keep-alive.
- fiber benchmark против gin — только на вашем плече нагрузки; синтетика TechEmpower — не ваш прод.

### Соберите порядок производственной диагностики

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

Правильный порядок строк:

1. `Зафиксировать baseline: go test -bench=. -benchmem и сохранить вывод в old.txt`
2. `Поднять pprof-сервер на localhost:6060 рядом с fiber`
3. `Снять CPU-профиль на 30 секунд под реальной нагрузкой`
4. `Снять профиль аллокаций: heap с -sample_index=alloc_objects`
5. `Найти главного виновника по колонке flat в pprof top`
6. `Оптимизировать одно место: структура вместо карты, omitempty, копии только там, где нужны`
7. `Перегнать бенчмарк и сохранить вывод в new.txt`
8. `Сравнить benchstat old.txt new.txt — изменение значимо, фиксируем`
9. `Открутить оптимизации, которые не дали значимого выигрыша, — код дороже героизма`

### Итоги: скорость как дисциплина, а не как маркетинг

Соберём урок в одну формулу. fasthttp производительность — это переиспользование: пулы контекстов и буферов, UnsafeString-конверсии, ноль аллокаций на горячем пути — и в обмен на это совместимость с net/http, HTTP/2 и ваша дисциплина не хранить ссылки на значения контекста. fiber vs gin — это не таблица бенчмарков, а выбор фундамента: экосистема и протоколы против скорости на узком плече. fiber benchmark — это ваш хендлер, app.Test, -benchmem и benchstat; pprof — это отдельный localhost-порт, тридцатисекундная CPU-выборка и колонка flat во главе решений; GC-ручки — GOGC, GOMEMLIMIT, ReduceMemoryUsage — последнее средство после чистки кода. Границы применимости переписали маркетинг: база и сериализация съедают выигрыш, большие тела требуют стриминга, HTTP/2 мимо. Честное слово про песочницу: ни fasthttp, ни fiber в браузерном интерпретаторе сайта не живут — runner-ов в уроках Fiber нет с первого урока, а интерактивы здесь quiz, predict, fill, order и флешкарты; бенчмарки и профили требуют локального тулчейна и нагрузки, похожей на боевую. Полный маршрут — на странице [курса Fiber](/courses/fiber/).

Следующий, двадцать пятый урок — финальный: итоговый проект «REST API заметок на Fiber»: CRUD, валидация, JWT, лимиты — готовое мини-приложение. Логика прямая: сегодня мы научились измерять и понимать движок, завтра соберём из двадцати четырёх уроков один проект, где слои, тесты, аутентификация и деплой-артефакты встретятся в одном main.go — и прогоним его от go mod init до curl через nginx.

### Карточки: fasthttp производительность

- **Zero memory allocations in hot paths** — Обещание fasthttp из README: пулы контекстов и буферов переиспользуются между запросами. Относится к накладным расходам движка, а не к вашему json.Marshal.
- **toString = utils.UnsafeString** — Дефолтная конверсия байтов в строки в fiber v3 без копии: значения c.Params/c.Get — алиасы в буфер соединения, действительны только до возврата из хендлера.
- **Immutable: true** — Поле fiber.Config: «relinquishes the 0-allocation promise» — копии вместо алиасов; значения переживают хендлер, цена — лишние аллокации.
- **go test -bench=. -benchmem + benchstat** — Штатный замер: ns/op, B/op, allocs/op; b.ReportAllocs() — колонка аллокаций в коде. Сравнение версий — только через benchstat, иначе шум.
- **pprof рядом с fiber** — _ "net/http/pprof" + http.ListenAndServe("localhost:6060", nil) в горутине: fiber не net/http, поэтому профили живут на соседнем порту. CPU 30с, heap с alloc_objects/alloc_space.
- **Границы fasthttp** — База и сериализация съедают выигрыш; большие тела — StreamRequestBody/DisablePreParseMultipartForm; HTTP/2 не поддерживается; http.Handler — только через adaptor.

## Коротко: что запомнить

- fasthttp производительность — это переиспользование: пулы контекстов и буферов, zero allocation на горячем пути движка; ваши аллокации (json.Marshal, слайсы, карты) остаются вашими и их покажет pprof.
- fiber аллокации под сделкой: toString по умолчанию — utils.UnsafeString, значения контекста живут до возврата хендлера («Do not store any references»); копии даёт Immutable: true ценой отказа от 0-allocation promise.
- fiber vs gin — выбор фундамента: net/http даёт HTTP/2 и экосистему http.Handler, fasthttp — скорость на мелких ответах и keep-alive; CRUD-профиль на паре тысяч RPS выбирает по совместимости, а не по синтетике.
- fiber benchmark — go test -bench=. -benchmem с b.ReportAllocs поверх app.Test; сравнение версий — benchstat; pprof — отдельный localhost-порт с net/http/pprof, читаем flat раньше cum.
- Границы применимости: база и сериализация нивелируют выигрыш, большие тела требуют StreamRequestBody и DisablePreParseMultipartForm, HTTP/2 движок не поддерживает; GC-ручки GOGC/GOMEMLIMIT/ReduceMemoryUsage — после чистки кода.

## Домашнее задание

Измерьте канон курса своими руками. Шаг первый: напишите BenchmarkListTasks по образцу урока — app.Test, b.ReportAllocs, проверка статуса — и сохраните вывод go test -bench=. -benchmem -benchtime=2s в old.txt. Шаг второй: поднимите pprof-сервер на localhost:6060 и снимите CPU-профиль на 30 секунд, одновременно нагружая /api/tasks циклом curl или ab. Шаг третий: в pprof top найдите главную функцию по flat и запишите, сколько процентов забирает сериализация против слоёв fiber и fasthttp. Шаг четвёртый: замените fiber.Map в ответе на структуру с json-тегами и omitempty, перегоните бенчмарк в new.txt и сравните benchstat old.txt new.txt — была ли разница значимой. Шаг пятый: попробуйте сохранить значение c.Params("id") в горутине и прогоните тест с -race — покажет ли детектор гонку или промолчит, объясните в комментарии, почему порча байтов реальна в обоих случаях; затем исправьте копией или Immutable. Бонус: включите GODEBUG=gctrace=1 и посчитайте число сборок до и после оптимизации.

## Вопросы и ответы по уроку

### Насколько Fiber реально быстрее Gin на продакшн-нагрузке?

На CRUD-сервисе с базой и тяжёлым JSON — обычно в пределах погрешности: время уходят хранилище и сериализация, а они одинаковы для обоих фреймворков. Выигрыш fasthttp раскрывается на плече «мелкие ответы, keep-alive, высокие RPS» — health-чеки, метрики, plaintext. Меряйте свой хендлер бенчмарком из урока и решайте по профилю, а не по таблицам TechEmpower.

### Почему нельзя просто сохранить c.Params("id") в глобальную переменную или горутину?

Потому что дефолтная конверсия — UnsafeString: строка не владеет байтами, а указывает в буфер соединения, который движок переиспользует после ответа. Комментарий исходника называет исход «silently corrupted on connection reuse» — тихо испорченные байты без паники и детектора гонок. Копия string(...) или Immutable: true делают значение честным.

### Почему /debug/pprof не работает прямо на fiber-порте?

Пакет net/http/pprof регистрирует хендлеры на DefaultServeMux стандартного net/http, а fiber на fasthttp — это другой движок с несовместимыми типами. Каноническое решение — крошечный http.ListenAndServe("localhost:6060", nil) в горутине рядом с боевым приложением: профилировщик слушает только локальный интерфейс и не попадает наружу.

### Когда включать GOMEMLIMIT и ReduceMemoryUsage?

GOMEMLIMIT — когда контейнер имеет жёсткий лимит памяти: мягкий потолок заставит GC работать до того, как OOM-killer убьёт процесс; ставьте ниже лимита контейнера. ReduceMemoryUsage — только когда профилирование показало, что память едят массы простаивающих keep-alive-соединений: поле честно предупреждает «at the cost of higher CPU usage». Обе ручки — после того, как чистка кода убрала лишние аллокации.
