Почему Laravel Excel ест гигабайты, а fast-excel-laravel — десятки мегабайт
Сценарий, знакомый почти каждому, кто выгружал отчёты в Excel из Laravel: на демо-данных всё летает, а в проде под конец месяца экспорт падает с Allowed memory size of 536870912 bytes exhausted либо висит так долго, что упирается в max_execution_time. Первая реакция — поднять memory_limit до гигабайта. Помогает до следующего роста данных.
Проблема не в вашем коде. Она в архитектуре движка, на котором построен самый популярный пакет для Excel в Laravel — maatwebsite/excel. Разберём, откуда берётся эта боль и почему avadim/fast-excel-laravel (версия 4.x) от неё свободен по построению.
Корень проблемы: «вся книга в памяти»
maatwebsite/excel — это тонкая и удобная обёртка вокруг PhpSpreadsheet. А PhpSpreadsheet устроен так: он строит полную объектную модель всей книги в оперативной памяти. Каждая ячейка — это PHP-объект со значением, типом, стилем и координатами. Пока файл не записан на диск, все эти объекты живут в памяти одновременно.
Для редактора документов это правильное решение: чтобы менять A1, потом Z100, потом снова A1, нужен произвольный доступ ко всем ячейкам. Но для типичной задачи «выгрузить 200 000 строк из базы в Excel» вы платите за эту свободу памятью, которая растёт линейно с числом ячеек. Это признано и в официальной документации самого пакета — PhpSpreadsheet держит значения всех ячеек в памяти, что на больших выгрузках приводит к проблемам.
Почему чанки не спасают
Стандартный совет — «читай данные из базы чанками» (FromQuery, chunk()). Это уменьшает память на стороне базы данных: вы не тянете всю выборку в коллекцию Eloquent разом. Но это не трогает вторую половину проблемы: сам Excel-документ всё равно целиком собирается в памяти до момента записи. Вы скормили движку данные порциями — а он всё равно накопил из них полное дерево объектов книги.
Отсюда типичная жалоба «я уже сделал чанки, а память всё равно кончается». Чанкинг выборки и потоковая запись файла — две разные вещи, и без второй первая не решает исходную задачу.
Как устроена потоковая выгрузка
avadim/fast-excel-laravel — обёртка над парой avadim/fast-excel-writer и avadim/fast-excel-reader, которые пишут и читают XLSX потоково. Ключевых механизма два, и работают они в связке.
Со стороны файла — запись «только вперёд». Записанная строка сразу сбрасывается во временный файл и покидает память. В каждый момент в памяти находится примерно одна текущая строка, а не вся книга. Память не растёт с числом строк.
Со стороны данных — чтение через cursor(). Метод exportModel() внутри обходит модель именно курсором Eloquent:
// упрощённо — то, что делает exportModel() под капотом
foreach ($model::cursor() as $record) {
yield $record;
}
cursor() держит в памяти одну модель за раз и не материализует всю выборку. В итоге обе стороны конвейера — и чтение из базы, и запись в файл — работают потоково, память остаётся постоянной независимо от объёма.
По бенчмарку из README нижележащего FastExcelWriter, на выгрузке того же порядка данных потоковая запись расходует память на порядок меньше, чем модель «вся книга в памяти», и заметно быстрее. Точные цифры зависят от данных и окружения, но принципиально важно другое: расход памяти перестаёт зависеть от числа строк.
Практика: экспорт, который не падает
Самый частый случай — выгрузить модель целиком. Здесь достаточно нескольких строк:
// Фасад \Excel регистрируется автоматически (package auto-discovery)
public function export()
{
$excel = \Excel::create('Users');
// Заголовки в первой строке + стили, затем потоковая выгрузка всей модели
$excel->sheet()
->withHeadings()
->applyFontStyleBold()
->applyBorder('thin')
->exportModel(User::class);
// Отдать файл браузером; временный файл удалится после отправки
return $excel->download('users.xlsx');
}
exportModel(User::class) пройдёт по всей таблице курсором и запишет строки одну за другой. Хоть 10 000 строк, хоть 2 000 000 — пик памяти будет одного порядка.
Если нужна не вся модель, а произвольная выборка или трансформация, есть writeData(), принимающий массив, коллекцию или генератор. Генератор — то, что нужно для больших объёмов, потому что он тоже отдаёт строки по одной:
$excel = \Excel::create('Orders');
$sheet = $excel->sheet();
$sheet->writeData(function () {
foreach (Order::where('paid', true)->lazy() as $order) {
yield [
'id' => $order->id,
'date' => $order->created_at,
'amount' => $order->amount,
];
}
});
$excel->saveTo('exports/orders.xlsx'); // относительно storage_path()
Ключевой момент: не вызывайте ->get() или ->all() перед выгрузкой — это материализует всю выборку в памяти и сведёт на нет весь смысл. Используйте cursor()/lazy() или сразу exportModel().
Таблица соответствия: с Laravel Excel на fast-excel-laravel
Если вы переносите существующий экспорт, вот соответствие типовых операций. Методы правой колонки — реальные, из API fast-excel-laravel 4.x.
| Задача | Laravel Excel (было) | fast-excel-laravel (стало) |
|---|---|---|
| Экспорт-класс | отдельный класс implements FromCollection |
не нужен — прямой вызов в контроллере/сервисе |
| Создать книгу | Excel::download(new UsersExport, 'u.xlsx') |
$excel = \Excel::create('Users'); |
| Выгрузить модель | FromQuery + query() |
$excel->sheet()->exportModel(User::class); |
| Выгрузить выборку | FromCollection + collection() |
$sheet->writeData($collection); или генератор |
| Заголовки | WithHeadings + headings() |
->withHeadings() или ->withHeadings(['A', 'B']) |
| Преобразование строки | WithMapping + map() |
->mapping(fn($m) => [...]) |
| Формат колонок | WithColumnFormatting |
->formatAttributes(['price' => '#,##0.00']) |
| Скачать браузером | Excel::download(...) |
$excel->download('users.xlsx'); |
| Сохранить на диск | Excel::store(new Export, 'file.xlsx', 's3') |
$excel->store('s3', 'path/file.xlsx'); |
Обратите внимание на первую строку: здесь нет отдельного класса-экспорта с набором интерфейсов. Логика выгрузки — это несколько fluent-вызовов там, где она нужна.
Честные границы
fast-excel-laravel — специализированный инструмент, а не замена PhpSpreadsheet на все случаи. Что нужно знать до перехода:
- Запись — только XLSX. Ни XLS, ни ODS, ни CSV на запись.
\Excel::create()всегда делает XLSX. (Чтение — шире: XLSX и устаревший XLS, формат определяется по сигнатуре файла.) - Запись «только вперёд». Нельзя записать строку, а потом вернуться и поправить уже сброшенную ячейку. Отчёты, которые формируются сверху вниз (шапка → данные → итог), — это почти все реальные выгрузки — переносятся легко. Для локального произвольного доступа есть области (
beginArea()), которые буферизуются целиком. - Формулы не вычисляются в PHP. Библиотека пишет текст формулы, а считает её Excel при открытии файла. Если готовое значение нужно прямо в файле, посчитайте его в PHP.
- Нет встроенной валидации импорта и очередей «из коробки». Валидацию строк и постановку тяжёлого импорта в очередь вы организуете сами средствами Laravel.
Правило простое: PhpSpreadsheet — редактор документов, fast-excel-laravel — быстрый генератор и импортёр отчётов. Если ваша задача — открыть готовый сложный файл, поправить в нём отдельные ячейки и сохранить, оставайтесь на PhpSpreadsheet: это инструмент под другую задачу.
Кому это подходит
Переход оправдан, если хотя бы один пункт про вас:
- экспорт падает по памяти или упирается в таймаут на реальных объёмах;
- отчёты растут, и вы уже поднимали
memory_limit«на будущее»; - выгрузка идёт из Eloquent-модели или большого запроса и формируется сверху вниз;
- хочется убрать инфраструктуру из классов-экспортов ради нескольких понятных вызовов.
А вот стабильную выгрузку на 50 строк, которая давно работает и никого не беспокоит, переписывать смысла нет.
Итог
- Падения по памяти на больших экспортах в Laravel Excel — следствие архитектуры PhpSpreadsheet «вся книга в памяти», а не вашего кода.
- Чанки на стороне базы не решают проблему: документ всё равно собирается в памяти целиком.
fast-excel-laravelпишет потоково и читает данные черезcursor()— память постоянна независимо от числа строк.- Цена — специализация: только запись XLSX, запись «только вперёд», формулы считает Excel. Для генерации отчётов это ровно то, что нужно.
composer require avadim/fast-excel-laravel
Репозиторий — github.com/aVadim483/fast-excel-laravel.