Cuando una exportación de 300,000 filas rebasa su presupuesto de memoria, normalmente se culpa al conteo de filas, y normalmente el conteo de filas es inocente. Las partes caras de un libro grande son las que se crean como efecto secundario: un pool de estilos que crece con una entrada por celda porque el formato se agregó dentro del bucle, XML de hoja ensamblado como una sola cadena gigante al guardar, un millón de cuerpos de fórmula idénticos almacenados uno por uno. HotXLS, la biblioteca nativa Delphi de losLab para archivos XLS y XLSX, les da una palanca específica para cada uno de estos costos. Ninguna está habilitada por defecto, porque cada una cambia un trade-off, así que saber qué palanca corresponde a qué síntoma es la verdadera habilidad de rendimiento.
Dónde gasta memoria un libro grande
Hay dos regímenes de memoria distintos que razonar. Durante la generación, el modelo de celdas en memoria crece con cada celda que tocan: valores, formatos y fórmulas se vuelven objetos o entradas de pool. Durante el guardado, la ruta XLSX predeterminada además renderiza el XML de cada hoja en una cadena wide antes de comprimirlo dentro del contenedor zip, así que el pico de uso es el modelo más la forma serializada de la hoja más grande. Un job que sobrevive al bucle de construcción y luego muere dentro de SaveAs está golpeando el segundo régimen, no el primero; y la solución de uno no hace nada por el otro.
El tamaño de archivo sigue una regla relacionada: las celdas son solo un contribuyente, junto con estilos, shared strings, fórmulas, imágenes y comentarios. Una pasada de auditoría con ForEachCell y los conteos de colecciones por hoja les dice qué recurso domina realmente un archivo problemático antes de optimizar el equivocado. Una sutileza de medición: Sheet.Cells.Count del lado XLSX informa el número de celdas instanciadas en el almacén disperso, no el área del rango usado; una hoja cuyos datos ocupan un rectángulo de 1000 por 50 con la mitad de las celdas vacías cuenta alrededor de 25,000, no 50,000. Esa distinción importa cuando comparan un archivo "enorme" de un cliente con sus fixtures, porque el área usada y la población real de celdas pueden diferir por un orden de magnitud en diseños financieros dispersos.
StreamingWrite corrige la ruta de guardado, no la ruta de construcción
Establecer TXLSXWorkbook.StreamingWrite := True cambia SaveAs a un serializador streaming que escribe el XML de la hoja directamente en el stream zip, eliminando la cadena intermedia por hoja. El valor predeterminado es False por compatibilidad de comportamiento, y activarlo es un cambio de una línea:
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
end;
Book.StreamingWrite := True; // sheet XML streams into the zip container
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Sean precisos sobre lo que esto compra: el modelo de celdas construido por el bucle ocupa exactamente la misma memoria que antes. StreamingWrite aplana el pico de memoria al guardar, que es la diferencia entre un job batch que completa y uno que falla al 95%; pero si el propio bucle de construcción agota memoria, las palancas que necesitan son las dos siguientes.
Pools de estilos: agregar una vez, reutilizar el índice
El formato XLSX en HotXLS se basa en pools: Book.Fonts.Add(...), Fills.AddSolid(...) y Borders.Add(...) devuelven un índice de pool 0-based que las celdas referencian. Llamar Fonts.Add con parámetros idénticos dentro de un bucle se deduplica, así que desperdicia tiempo más que espacio; pero Alignments.Add devuelve un objeto fresco por llamada, por lo que crear alineaciones por celda hace crecer el pool linealmente con el conteo de filas. El hábito robusto cubre ambos casos: resuelvan cada índice de pool una vez, fuera del bucle, y asignen índices dentro de él.
// hoist pool lookups out of the hot loop
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False); // 0-based pool index
for C := 1 to 24 do
Sheet.Cells[1, C].FontIndex := HeaderFont + 1; // cells store 1-based; 0 = default
El + 1 no es un typo, y olvidarlo es el bug clásico que genera síntomas aquí: los pools entregan índices 0-based, mientras que las propiedades del lado celda tratan 0 como "predeterminado", así que cada índice de pool debe desplazarse en uno al asignar. Equivóquense por omisión y los encabezados se renderizarán en silencio con la fuente predeterminada del libro, un defecto que nadie nota hasta la revisión de marca.
Reemplazar tráfico Variant por celda con callbacks de fila
Cada Sheet.Cells[R, C].Value := X implica una búsqueda o creación de celda más una asignación Variant. A unos cuantos cientos de miles de celdas, ese overhead por acceso se vuelve medible en perfiles. HotXLS proporciona APIs bulk con callbacks en ambas fachadas, ForEachCell y ForEachRow para lectura, WriteCells y WriteRows para escritura, que mueven la iteración dentro del motor y entregan a su código filas completas a la vez:
procedure TLedgerExport.FillRow(Sender: TObject;
SheetIndex, Row, FirstCol, LastCol: Integer;
var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
if Row > FCount then
begin
Cancel := True; // stop the whole write
Exit;
end;
Values := VarArrayOf([FRows[Row - 1].Account,
FRows[Row - 1].PostedOn,
FRows[Row - 1].Amount]);
end;
// one engine call instead of hundreds of thousands of property hits
Sheet.WriteRows(1, 1, FCount, 3, FillRow);
La bandera Skip del callback deja una fila sin tocar sin abortar, y Cancel termina la operación antes de tiempo, útil cuando la fuente es un reader cuya longitud descubren conforme avanzan. Combinen WriteRows para la construcción con StreamingWrite para el guardado y la ruta de generación no conserva ningún hot spot por celda.
Palancas de lectura en la fachada XLS
Los archivos .xls heredados grandes tienen su propio kit. _DisableGraphics := True antes de Open omite por completo el análisis de la capa de dibujo, lo que acelera la carga de libros que llevan años de formas e imágenes incrustadas acumuladas, con una restricción dura: la capa de dibujo queda ausente del modelo, así que guardar ese libro escribe un archivo sin sus dibujos. Reserven esta bandera para trabajos de análisis de solo lectura. SetTempDir redirige los archivos temporales del escritor BIFF, importante en servidores donde la ubicación temporal predeterminada tiene cuota o está en almacenamiento lento. UseSharedFormulas agrupa cuerpos de fórmula repetidos en registros de fórmula compartida, reduciendo archivos donde una columna de fórmula se repite por sesenta mil filas.
Los bucles de lectura sobre datos XLS tienen una trampa de indexación que vale señalar porque duplica trabajo cuando se maneja defensivamente y corrompe resultados cuando se omite: UsedRange informa sus límites FirstRow, LastRow, FirstCol y LastCol como 0-based, mientras que Cells.Item[Row, Col] es 1-based. Un escaneo que recorre el rango usado debe sumar uno a cada coordenada en el acceso a celda, Cells.Item[Row + 1, Col + 1], o lee una cuadrícula desplazada diagonalmente una celda, descartando en silencio la última fila y columna e incluyendo una primera fantasma. El callback ForEachCell evita por completo la discrepancia, otra razón para preferirlo en escaneos de hoja completa.
Sondear archivos antes de cargarlos
La operación más barata sobre un libro grande es la que evitan. GetSheetNames en ambas fachadas lista las hojas de un archivo sin cargar datos de celda: la implementación XLSX lee solo el manifest del libro dentro del zip y deja explícitamente la instancia de libro sin poblar, y la fachada XLS detiene el escaneo en el primer límite de subflujo. Eso la convierte en la verificación previa correcta para "qué hoja debe apuntar este job de importación", y CanReadEncrypted responde "¿es este un contenedor cifrado?" antes de un intento de Open condenado.
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
raise Exception.Create('cannot enumerate sheets'); // failure clears the list
// pick the target sheet, then decide whether a full Open is worth it
finally
Book.Free;
Names.Free;
end;
Noten la convención de código de retorno: estas funciones de sondeo señalan falla con valores menores o iguales a cero y vacían la lista de salida, así que prueben <= 0 en lugar de comparar contra un valor específico de éxito.
Ajustar el enfoque al trabajo
Para pipelines desatendidos que generan muchos archivos grandes en secuencia, dos hábitos más completan el panorama. Los objetos de libro no son thread-safe para compartirse, pero nada impide usar un libro independiente por hilo worker, lo que paraleliza limpiamente la conversión batch. Y cuando la salida va a HTTP en vez de disco, los overloads de guardado a TStream se combinan con StreamingWrite para que una respuesta grande nunca se materialice como archivo temporal, con una nota operativa: el guardado en stream escribe desde la posición actual sin rebobinar, así que establezcan Position := 0 antes de entregar el stream al framework de respuesta. El artículo de streaming write y jobs batch desarrolla ese patrón del lado servidor, y el artículo de exportación de base de datos muestra dónde encajan estas palancas en un reporte impulsado por datasets.
Por último, mantengan un fixture de peor caso por familia de reportes y midan su tiempo en CI. Las regresiones de rendimiento en generación de documentos rara vez se anuncian: un estilo agregado dentro de un bucle o un sondeo reemplazado por un Open completo no cambia nada funcionalmente, y el batch nocturno simplemente tarda cuarenta minutos más. Una prueba cronometrada sobre un fixture representativo de medio millón de celdas convierte esa deriva en un build rojo en lugar de un incidente de operaciones.
Las builds de evaluación, proyectos demo con un ejemplo de generación masiva y la referencia completa de API están disponibles en la página HotXLS Component.