From 63d5cbebe7728c6912b19c4c83e119207d4d677c Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Fri, 11 Sep 2026 14:21:04 -0300 Subject: [PATCH] melhorias de performance --- backend/src/server.ts | 9 ++++-- data-pipeline/scripts/extract-manifest.mjs | 18 +++++++++-- frontend/src/App.tsx | 36 +++++++++++++--------- nginx/nginx.conf | 15 +++++++++ 4 files changed, 59 insertions(+), 19 deletions(-) diff --git a/backend/src/server.ts b/backend/src/server.ts index e2607ac..52937ea 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -20,12 +20,17 @@ const app = Fastify({ logger: true, trustProxy: true }); await app.register(cors, { origin: true }); +// Cache-Control é decidido no nginx interno (ver nginx/nginx.conf, +// location /models/), não aqui — "immutable"/maxAge nesse nível conflitava +// com o "no-cache" que o nginx adiciona por cima (dois headers +// Cache-Control na mesma resposta, comportamento indefinido entre +// navegadores), e ia contra o motivo do "no-cache": os .glb têm nome fixo +// mas o CONTEÚDO muda toda vez que o pipeline roda de novo. await app.register(fastifyStatic, { root: MODELS_DIR, prefix: "/models/", decorateReply: false, - immutable: true, - maxAge: "7d", + etag: true, }); app.get("/api/health", async () => ({ status: "ok" })); diff --git a/data-pipeline/scripts/extract-manifest.mjs b/data-pipeline/scripts/extract-manifest.mjs index 9f739f2..4f42fba 100644 --- a/data-pipeline/scripts/extract-manifest.mjs +++ b/data-pipeline/scripts/extract-manifest.mjs @@ -6,8 +6,8 @@ // - data-pipeline/seed/manifest-real.json (entradas para o seed do banco) import { NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; -import { prune, dedup, weld, simplify } from "@gltf-transform/functions"; -import { MeshoptSimplifier } from "meshoptimizer"; +import { prune, dedup, weld, simplify, meshopt } from "@gltf-transform/functions"; +import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer"; import { writeFileSync, mkdirSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -21,6 +21,7 @@ import { MUSCLE_DESCRIPTIONS } from "./muscle-descriptions.mjs"; import { NERVE_DESCRIPTIONS } from "./nerve-descriptions.mjs"; await MeshoptSimplifier.ready; +await MeshoptEncoder.ready; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const CONVERTED_DIR = path.resolve(__dirname, "../converted"); @@ -183,7 +184,12 @@ function classifySystem(key, rawName) { return RESPIRATORY_KEYWORDS.some((kw) => lower.includes(kw)) ? "respiratorio" : "visceral"; } -const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); +// registerDependencies com a chave "meshopt.encoder" é o que permite ao +// io.write() de fato aplicar a compressão EXT_meshopt_compression pedida +// no transform() abaixo — sem isso, ela seria ignorada silenciosamente. +const io = new NodeIO() + .registerExtensions(ALL_EXTENSIONS) + .registerDependencies({ "meshopt.encoder": MeshoptEncoder }); const manifest = []; let totalStructures = 0; @@ -349,6 +355,12 @@ for (const { fbx, key } of SOURCES) { simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }), prune(), dedup(), + // Comprime os atributos de vértice (posição/normal/UV) com o mesmo + // codec Meshopt já usado pra simplificação — reduz bastante o tamanho + // do .glb final (os floats crus de posição/normal dominam o arquivo), + // e o carregamento no navegador não precisa de nada extra: o useGLTF + // do drei já vem com o decoder Meshopt ligado por padrão. + meshopt({ encoder: MeshoptEncoder, level: "medium" }), ); const outPath = path.join(MODELS_OUT_DIR, target.modelFile); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 14cd830..e25ec82 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,22 +1,30 @@ +import { lazy, Suspense } from "react"; import { Route, Routes } from "react-router-dom"; import { Layout } from "./components/Layout.js"; + +// A página inicial (Atlas) já carrega o peso pesado de verdade (three.js + +// r3f + drei) — separar as OUTRAS rotas em chunks à parte só evita que +// quem abre o atlas baixe/parseie de graça o código do Quiz, de Planos e +// do painel interno de estatísticas antes mesmo de ver o modelo 3D. import AtlasPage from "./routes/AtlasPage.js"; -import PlanesPage from "./routes/PlanesPage.js"; -import QuizPage from "./routes/QuizPage.js"; -import PhysicalExamPage from "./routes/PhysicalExamPage.js"; -import AdminStatsPage from "./routes/AdminStatsPage.js"; +const PlanesPage = lazy(() => import("./routes/PlanesPage.js")); +const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js")); +const QuizPage = lazy(() => import("./routes/QuizPage.js")); +const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js")); export default function App() { return ( - - }> - } /> - } /> - } /> - } /> - - {/* Fora do Layout: painel interno, sem o menu/nav do atlas. */} - } /> - + + + }> + } /> + } /> + } /> + } /> + + {/* Fora do Layout: painel interno, sem o menu/nav do atlas. */} + } /> + + ); } diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2a7bcc2..0a66875 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -11,6 +11,13 @@ http { keepalive_timeout 65; gzip on; gzip_types text/plain text/css application/javascript application/json application/wasm model/gltf-binary; + # Sem isso, o default do nginx (gzip_proxied off) desliga a compressão + # justamente nas respostas que passam por proxy_pass (/api/ e /models/, + # que são as maiores — JSON de estruturas e os .glb) — só os arquivos + # estáticos servidos direto por este nginx (JS/CSS do build) estavam + # sendo comprimidos até agora. + gzip_proxied any; + gzip_vary on; # Respect a proxy in front of this container (the user's own reverse proxy). set_real_ip_from 0.0.0.0/0; @@ -72,6 +79,14 @@ http { add_header Cache-Control "no-cache"; } + # Bundle do Vite (JS/CSS/fonts) tem hash de conteúdo no nome do arquivo + # — ao contrário dos .glb, um arquivo com esse nome NUNCA muda de + # conteúdo (um build novo gera nomes novos), então pode cachear "pra + # sempre" sem risco de servir algo desatualizado. + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + # SPA fallback. location / { try_files $uri $uri/ /index.html;