perf: adiciona gzip compression e otimiza carregamento
- Adiciona middleware compression para servir assets gzipados - Configura cache de 1 ano para assets estáticos (hash no filename) - Remove imports pesados do Agent.tsx (jspdf, docx, recharts) - Cria ChartRenderer com lazy loading para gráficos - Cria export-utils com dynamic imports para PDF/Word/CSV Reduz o chunk Agent de 893KB para 690KB (gzip 220KB).
This commit is contained in:
parent
6bd0ea6d4f
commit
a167d9aaef
|
|
@ -0,0 +1,146 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
|
||||
interface ChartData {
|
||||
type: "bar" | "line" | "pie" | "area";
|
||||
title: string;
|
||||
data: Record<string, any>[];
|
||||
xKey: string;
|
||||
yKeys: string[];
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
// Lazy load do recharts para não pesar o bundle inicial
|
||||
const RechartsComponents = lazy(() =>
|
||||
import("recharts").then((module) => ({
|
||||
default: function RechartsWrapper({ chart }: { chart: ChartData }) {
|
||||
const {
|
||||
BarChart,
|
||||
Bar,
|
||||
LineChart,
|
||||
Line,
|
||||
PieChart,
|
||||
Pie,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} = module;
|
||||
|
||||
const renderChart = () => {
|
||||
switch (chart.type) {
|
||||
case "bar":
|
||||
return (
|
||||
<BarChart data={chart.data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={chart.xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
{chart.yKeys.map((key, index) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
fill={chart.colors[index % chart.colors.length]}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
);
|
||||
case "line":
|
||||
return (
|
||||
<LineChart data={chart.data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={chart.xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
{chart.yKeys.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={chart.colors[index % chart.colors.length]}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
);
|
||||
case "area":
|
||||
return (
|
||||
<AreaChart data={chart.data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={chart.xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
{chart.yKeys.map((key, index) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={chart.colors[index % chart.colors.length]}
|
||||
fill={chart.colors[index % chart.colors.length]}
|
||||
fillOpacity={0.6}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
);
|
||||
case "pie":
|
||||
return (
|
||||
<PieChart>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Pie
|
||||
data={chart.data}
|
||||
dataKey={chart.yKeys[0]}
|
||||
nameKey={chart.xKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label
|
||||
>
|
||||
{chart.data.map((_, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={chart.colors[index % chart.colors.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{renderChart()}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
interface ChartRendererProps {
|
||||
chart: ChartData;
|
||||
}
|
||||
|
||||
export function ChartRenderer({ chart }: ChartRendererProps) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-64 flex items-center justify-center bg-muted/20 rounded">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RechartsComponents chart={chart} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
// Utilitários de exportação com lazy loading dinâmico
|
||||
// Reduz o bundle inicial do Agent.tsx
|
||||
|
||||
export async function exportToPDF(content: string, prompt?: string, chartContainer?: HTMLElement | null) {
|
||||
const [{ jsPDF }, html2canvas] = await Promise.all([
|
||||
import("jspdf"),
|
||||
import("html2canvas").then(m => m.default)
|
||||
]);
|
||||
|
||||
const doc = new jsPDF();
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 20;
|
||||
const maxWidth = pageWidth - margin * 2;
|
||||
|
||||
doc.setFontSize(16);
|
||||
doc.setTextColor(31, 51, 77);
|
||||
doc.text("Arcádia Agent - Resultado", margin, 20);
|
||||
|
||||
let currentY = 30;
|
||||
|
||||
if (prompt) {
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(90, 108, 125);
|
||||
doc.text("Tarefa:", margin, currentY);
|
||||
currentY += 7;
|
||||
const promptLines = doc.splitTextToSize(prompt, maxWidth);
|
||||
doc.text(promptLines, margin, currentY);
|
||||
currentY += promptLines.length * 5 + 10;
|
||||
}
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(31, 51, 77);
|
||||
doc.text("Resposta:", margin, currentY);
|
||||
currentY += 8;
|
||||
|
||||
const textContent = content.replace(/__CHART_DATA__[\s\S]*?__END_CHART_DATA__/g, "").trim();
|
||||
doc.setFontSize(11);
|
||||
const lines = doc.splitTextToSize(textContent, maxWidth);
|
||||
doc.text(lines, margin, currentY);
|
||||
currentY += lines.length * 5 + 10;
|
||||
|
||||
// Verifica se há dados de gráfico e captura
|
||||
const chartDataMatch = content.match(/__CHART_DATA__([\s\S]*?)__END_CHART_DATA__/);
|
||||
if (chartDataMatch && chartContainer) {
|
||||
try {
|
||||
const canvas = await html2canvas(chartContainer, {
|
||||
backgroundColor: "#ffffff",
|
||||
scale: 2,
|
||||
});
|
||||
const imgData = canvas.toDataURL("image/png");
|
||||
const imgWidth = maxWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
|
||||
if (currentY + imgHeight > doc.internal.pageSize.getHeight() - margin) {
|
||||
doc.addPage();
|
||||
currentY = margin;
|
||||
}
|
||||
|
||||
doc.addImage(imgData, "PNG", margin, currentY, imgWidth, imgHeight);
|
||||
} catch (e) {
|
||||
console.error("Error capturing chart:", e);
|
||||
}
|
||||
}
|
||||
|
||||
doc.save("arcadia-resultado.pdf");
|
||||
}
|
||||
|
||||
export async function exportToWord(content: string, prompt?: string) {
|
||||
const { Document, Packer, Paragraph, TextRun } = await import("docx");
|
||||
const { saveAs } = await import("file-saver");
|
||||
|
||||
const paragraphs = [];
|
||||
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "Arcádia Agent - Resultado", bold: true, size: 32 })],
|
||||
})
|
||||
);
|
||||
|
||||
if (prompt) {
|
||||
paragraphs.push(
|
||||
new Paragraph({ children: [] }),
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "Tarefa: ", bold: true }), new TextRun({ text: prompt })],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
paragraphs.push(
|
||||
new Paragraph({ children: [] }),
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "Resposta:", bold: true })],
|
||||
})
|
||||
);
|
||||
|
||||
content.split("\n").forEach((line) => {
|
||||
paragraphs.push(new Paragraph({ children: [new TextRun({ text: line })] }));
|
||||
});
|
||||
|
||||
const doc = new Document({
|
||||
sections: [{ properties: {}, children: paragraphs }],
|
||||
});
|
||||
|
||||
const blob = await Packer.toBlob(doc);
|
||||
saveAs(blob, "arcadia-resultado.docx");
|
||||
}
|
||||
|
||||
export async function exportToText(content: string, prompt?: string) {
|
||||
const { saveAs } = await import("file-saver");
|
||||
const textContent = prompt ? `Tarefa: ${prompt}\n\nResposta:\n${content}` : content;
|
||||
const blob = new Blob([textContent], { type: "text/plain;charset=utf-8" });
|
||||
saveAs(blob, "arcadia-resultado.txt");
|
||||
}
|
||||
|
||||
export async function exportToCSV(content: string) {
|
||||
const { saveAs } = await import("file-saver");
|
||||
const lines = content.split("\n");
|
||||
let csvContent = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes("|")) {
|
||||
const cells = line
|
||||
.split("|")
|
||||
.map((cell) => cell.trim())
|
||||
.filter((cell) => cell && !cell.match(/^[-:]+$/));
|
||||
if (cells.length > 0) {
|
||||
csvContent += cells.map((cell) => `"${cell.replace(/"/g, """)}"`).join(",") + "\n";
|
||||
}
|
||||
} else if (line.includes("\t")) {
|
||||
csvContent +=
|
||||
line
|
||||
.split("\t")
|
||||
.map((cell) => `"${cell.trim().replace(/"/g, """)}"`)
|
||||
.join(",") + "\n";
|
||||
} else if (line.trim()) {
|
||||
csvContent += `"${line.replace(/"/g, """)}"\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8" });
|
||||
saveAs(blob, "arcadia-resultado.csv");
|
||||
}
|
||||
|
|
@ -54,13 +54,10 @@ import {
|
|||
} from "lucide-react";
|
||||
import { CardDescription } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { jsPDF } from "jspdf";
|
||||
import { Document, Packer, Paragraph, TextRun } from "docx";
|
||||
import { saveAs } from "file-saver";
|
||||
import html2canvas from "html2canvas";
|
||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from "recharts";
|
||||
import { ResultViewer } from "@/components/ResultViewer";
|
||||
import DevHistory from "@/components/DevHistory";
|
||||
import { ChartRenderer } from "@/components/ChartRenderer";
|
||||
import { exportToPDF, exportToWord, exportToText, exportToCSV } from "@/lib/export-utils";
|
||||
|
||||
interface ChartData {
|
||||
type: 'bar' | 'line' | 'pie' | 'area';
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bson": "^4.0.5",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/imap": "^0.8.43",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx": "^9.5.1",
|
||||
|
|
@ -7300,6 +7302,16 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
|
|
@ -8506,6 +8518,69 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bson": "^4.0.5",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/imap": "^0.8.43",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx": "^9.5.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import compression from "compression";
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(__dirname, "public");
|
||||
|
|
@ -10,7 +11,22 @@ export function serveStatic(app: Express) {
|
|||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
// Enable gzip compression for all responses
|
||||
app.use(compression({
|
||||
level: 6, // balance between speed and compression
|
||||
filter: (req, res) => {
|
||||
// Compress all JSON and static assets
|
||||
if (req.headers['x-no-compression']) return false;
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
}));
|
||||
|
||||
// Serve static files with proper caching
|
||||
app.use(express.static(distPath, {
|
||||
maxAge: '1y', // Cache static assets for 1 year (they have hash in filename)
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
}));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue