melhorias de performance

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-11 14:21:04 -03:00
1 parent ce851b9cf9
commit 63d5cbebe7
4 files changed
+49 -9

No files matched your search

+7 -2
View File
@@ -20,12 +20,17 @@ const app = Fastify({ logger: true, trustProxy: true });
await app.register(cors, { origin: 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, { await app.register(fastifyStatic, {
root: MODELS_DIR, root: MODELS_DIR,
prefix: "/models/", prefix: "/models/",
decorateReply: false, decorateReply: false,
immutable: true, etag: true,
maxAge: "7d",
}); });
app.get("/api/health", async () => ({ status: "ok" })); app.get("/api/health", async () => ({ status: "ok" }));
+15 -3
View File
@@ -6,8 +6,8 @@
// - data-pipeline/seed/manifest-real.json (entradas para o seed do banco) // - data-pipeline/seed/manifest-real.json (entradas para o seed do banco)
import { NodeIO } from "@gltf-transform/core"; import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { prune, dedup, weld, simplify } from "@gltf-transform/functions"; import { prune, dedup, weld, simplify, meshopt } from "@gltf-transform/functions";
import { MeshoptSimplifier } from "meshoptimizer"; import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
import { writeFileSync, mkdirSync } from "node:fs"; import { writeFileSync, mkdirSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -21,6 +21,7 @@ import { MUSCLE_DESCRIPTIONS } from "./muscle-descriptions.mjs";
import { NERVE_DESCRIPTIONS } from "./nerve-descriptions.mjs"; import { NERVE_DESCRIPTIONS } from "./nerve-descriptions.mjs";
await MeshoptSimplifier.ready; await MeshoptSimplifier.ready;
await MeshoptEncoder.ready;
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONVERTED_DIR = path.resolve(__dirname, "../converted"); 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"; 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 = []; const manifest = [];
let totalStructures = 0; let totalStructures = 0;
@@ -349,6 +355,12 @@ for (const { fbx, key } of SOURCES) {
simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }), simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }),
prune(), prune(),
dedup(), 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); const outPath = path.join(MODELS_OUT_DIR, target.modelFile);
+12 -4
View File
@@ -1,13 +1,20 @@
import { lazy, Suspense } from "react";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { Layout } from "./components/Layout.js"; 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 AtlasPage from "./routes/AtlasPage.js";
import PlanesPage from "./routes/PlanesPage.js"; const PlanesPage = lazy(() => import("./routes/PlanesPage.js"));
import QuizPage from "./routes/QuizPage.js"; const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js"));
import PhysicalExamPage from "./routes/PhysicalExamPage.js"; const QuizPage = lazy(() => import("./routes/QuizPage.js"));
import AdminStatsPage from "./routes/AdminStatsPage.js"; const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js"));
export default function App() { export default function App() {
return ( return (
<Suspense fallback={null}>
<Routes> <Routes>
<Route element={<Layout />}> <Route element={<Layout />}>
<Route path="/" element={<AtlasPage />} /> <Route path="/" element={<AtlasPage />} />
@@ -18,5 +25,6 @@ export default function App() {
{/* Fora do Layout: painel interno, sem o menu/nav do atlas. */} {/* Fora do Layout: painel interno, sem o menu/nav do atlas. */}
<Route path="/estatisticas" element={<AdminStatsPage />} /> <Route path="/estatisticas" element={<AdminStatsPage />} />
</Routes> </Routes>
</Suspense>
); );
} }
+15
View File
@@ -11,6 +11,13 @@ http {
keepalive_timeout 65; keepalive_timeout 65;
gzip on; gzip on;
gzip_types text/plain text/css application/javascript application/json application/wasm model/gltf-binary; 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). # Respect a proxy in front of this container (the user's own reverse proxy).
set_real_ip_from 0.0.0.0/0; set_real_ip_from 0.0.0.0/0;
@@ -72,6 +79,14 @@ http {
add_header Cache-Control "no-cache"; 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. # SPA fallback.
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;