perf(cache): migração para diretiva use cache do Next.js

Todas as queries cacheadas do dashboard migram de `unstable_cache` para
a diretiva `use cache` com `cacheTag` e `cacheLife({ revalidate: 3 })`.

Todas as páginas e o layout do dashboard passam a chamar `connection()`
para garantir renderização dinâmica. O root layout envolve os filhos em
`<Suspense>`. `next.config.ts` remove `turbopackFileSystemCacheForDev`
e adota `cacheComponents: true`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-01 14:14:23 +00:00
parent 96df6a1798
commit e32fb85006
30 changed files with 86 additions and 54 deletions

View File

@@ -6,9 +6,7 @@ dotenv.config();
const nextConfig: NextConfig = {
output: "standalone",
experimental: {
turbopackFileSystemCacheForDev: true,
},
cacheComponents: true,
reactCompiler: true,
images: {
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],

View File

@@ -1,5 +1,6 @@
import { RiPencilLine } from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { AccountDialog } from "@/features/accounts/components/account-dialog";
import { AccountStatementCard } from "@/features/accounts/components/account-statement-card";
import type { Account } from "@/features/accounts/components/types";
@@ -42,6 +43,7 @@ const capitalize = (value: string) =>
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { accountId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { AccountsPage } from "@/features/accounts/components/accounts-page";
import { fetchAllAccountsForUser } from "@/features/accounts/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeAccounts, archivedAccounts, logoOptions } =
await fetchAllAccountsForUser(userId);

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { BudgetsPage } from "@/features/budgets/components/budgets-page";
import { fetchBudgetsForUser } from "@/features/budgets/queries";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
@@ -23,6 +24,7 @@ const capitalize = (value: string) =>
value.length === 0 ? value : value[0]?.toUpperCase() + value.slice(1);
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { MonthlyCalendar } from "@/features/calendar/components/monthly-calendar";
import { fetchCalendarData } from "@/features/calendar/queries";
import {
@@ -16,6 +17,7 @@ type PageProps = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedParams = searchParams ? await searchParams : undefined;

View File

@@ -1,5 +1,6 @@
import { RiPencilLine } from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import type { FinancialAccount } from "@/db/schema";
import { CardDialog } from "@/features/cards/components/card-dialog";
import type { Card } from "@/features/cards/components/types";
@@ -39,6 +40,7 @@ type PageProps = {
};
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { cardId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { CardsPage } from "@/features/cards/components/cards-page";
import { fetchAllCardsForUser } from "@/features/cards/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeCards, archivedCards, accounts, logoOptions } =
await fetchAllCardsForUser(userId);

View File

@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header";
import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries";
import { fetchUserPreferences } from "@/features/settings/queries";
@@ -32,6 +33,7 @@ const getSingleParam = (
};
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { categoryId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,9 +1,11 @@
import { connection } from "next/server";
import { fetchCategoryHistory } from "@/features/dashboard/categories/category-history-queries";
import { CategoryHistoryWidget } from "@/features/dashboard/components/category-history-widget";
import { getUser } from "@/shared/lib/auth/server";
import { getCurrentPeriod } from "@/shared/utils/period";
export default async function HistoricoCategoriasPage() {
await connection();
const user = await getUser();
const currentPeriod = getCurrentPeriod();

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { CategoriesPage } from "@/features/categories/components/categories-page";
import { fetchCategoriesForUser } from "@/features/categories/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const categories = await fetchCategoriesForUser(userId);

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
@@ -14,6 +15,7 @@ type PageProps = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { InboxPage } from "@/features/inbox/components/inbox-page";
import {
type ResolvedInboxSearchParams,
@@ -31,6 +32,7 @@ const EMPTY_DIALOG_DATA = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const activeStatus = resolveInboxStatus(resolvedSearchParams);

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { InsightsPage } from "@/features/insights/components/insights-page";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { parsePeriodParam } from "@/shared/utils/period";
@@ -18,6 +19,7 @@ const getSingleParam = (
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParam);

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
@@ -9,6 +10,7 @@ export default async function DashboardLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
await connection();
const session = await getUserSession();
const navbarData = await fetchDashboardNavbarData(session.user.id);

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { NotesPage } from "@/features/notes/components/notes-page";
import { fetchAllNotesForUser } from "@/features/notes/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);

View File

@@ -4,6 +4,7 @@ import {
RiWallet3Line,
} from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
import { PayerHistoryCard } from "@/features/payers/components/details/payer-history-card";
@@ -91,6 +92,7 @@ const createEmptySlugMaps = (): SlugMaps => ({
type OptionSet = ReturnType<typeof buildOptionSets>;
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { payerId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { PayersPage } from "@/features/payers/components/payers-page";
import { fetchPayersForUser } from "@/features/payers/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { payers, avatarOptions } = await fetchPayersForUser(userId);

View File

@@ -1,4 +1,5 @@
import { RiBankCard2Line } from "@remixicon/react";
import { connection } from "next/server";
import { fetchCartoesReportData } from "@/features/reports/cards-report-queries";
import { CardCategoryBreakdown } from "@/features/reports/components/cards/card-category-breakdown";
import { CardInvoiceStatus } from "@/features/reports/components/cards/card-invoice-status";
@@ -28,6 +29,7 @@ const getSingleParam = (
export default async function RelatorioCartoesPage({
searchParams,
}: PageProps) {
await connection();
const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");

View File

@@ -1,4 +1,5 @@
import { redirect } from "next/navigation";
import { connection } from "next/server";
import type { Category } from "@/db/schema";
import { fetchCategoryChartData } from "@/features/reports/category-chart-queries";
import { fetchCategoryReport } from "@/features/reports/category-report-queries";
@@ -29,6 +30,7 @@ const getSingleParam = (
};
export default async function Page({ searchParams }: PageProps) {
await connection();
// Get authenticated user
const userId = await getUserId();

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { EstablishmentsList } from "@/features/reports/components/establishments/establishments-list";
import { HighlightsCards } from "@/features/reports/components/establishments/highlights-cards";
import { PeriodFilterButtons } from "@/features/reports/components/establishments/period-filter";
@@ -36,6 +37,7 @@ const validatePeriodFilter = (value: string | null): PeriodFilter => {
export default async function TopEstabelecimentosPage({
searchParams,
}: PageProps) {
await connection();
const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { InstallmentAnalysisPage } from "@/features/dashboard/components/installment-analysis/installment-analysis-page";
import { fetchInstallmentAnalysis } from "@/features/dashboard/expenses/installment-analysis-queries";
import { getUser } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const user = await getUser();
const data = await fetchInstallmentAnalysis(user.id);

View File

@@ -1,6 +1,7 @@
import { RiAndroidLine, RiArrowRightSLine } from "@remixicon/react";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { connection } from "next/server";
import { CompanionTab } from "@/features/settings/components/companion-tab";
import { DeleteAccountForm } from "@/features/settings/components/delete-account-form";
@@ -21,6 +22,7 @@ import {
import { auth } from "@/shared/lib/auth/config";
export default async function Page() {
await connection();
const session = await auth.api.getSession({
headers: await headers(),
});
@@ -65,7 +67,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1">Preferências</h2>
<h2 className="text-xl font-medium mb-1">Preferências</h2>
<p className="text-sm text-muted-foreground">
Personalize sua experiência no OpenMonetis ajustando as
configurações de acordo com suas necessidades.
@@ -90,7 +92,7 @@ export default async function Page() {
<div className="space-y-4">
<div>
<div className="flex items-center gap-2 mb-1">
<h2 className="text-xl font-bold">OpenMonetis Companion</h2>
<h2 className="text-xl font-medium">OpenMonetis Companion</h2>
<span className="inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-xs font-medium text-success dark:bg-success/10">
<RiAndroidLine className="h-3 w-3" />
Android
@@ -112,7 +114,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1">Alterar nome</h2>
<h2 className="text-xl font-medium mb-1">Alterar nome</h2>
<p className="text-sm text-muted-foreground">
Atualize como seu nome aparece no OpenMonetis. Esse nome pode
ser exibido em diferentes seções do app e em comunicações.
@@ -128,7 +130,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1">Alterar senha</h2>
<h2 className="text-xl font-medium mb-1">Alterar senha</h2>
<p className="text-sm text-muted-foreground">
Defina uma nova senha para sua conta. Guarde-a em local
seguro.
@@ -144,7 +146,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1">Passkeys</h2>
<h2 className="text-xl font-medium mb-1">Passkeys</h2>
<p className="text-sm text-muted-foreground">
Passkeys permitem login sem senha, usando biometria (Face ID,
Touch ID, Windows Hello) ou chaves de segurança.
@@ -160,7 +162,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1">Alterar e-mail</h2>
<h2 className="text-xl font-medium mb-1">Alterar e-mail</h2>
<p className="text-sm text-muted-foreground">
Atualize o e-mail associado à sua conta. Você precisará
confirmar os links enviados para o novo e também para o e-mail
@@ -180,7 +182,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold mb-1 text-destructive">
<h2 className="text-xl font-medium mb-1 text-destructive">
Ações perigosas
</h2>
<p className="text-sm text-muted-foreground">

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { ImportPage } from "@/features/transactions/components/import/import-page";
import {
buildOptionSets,
@@ -7,6 +8,7 @@ import { fetchTransactionFilterSources } from "@/features/transactions/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const filterSources = await fetchTransactionFilterSources(userId);
const sluggedFilters = buildSluggedFilters(filterSources);

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { fetchUserPreferences } from "@/features/settings/queries";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import {
@@ -27,6 +28,7 @@ type PageProps = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { ThemeProvider } from "@/shared/components/providers/theme-provider";
import { Toaster } from "@/shared/components/ui/sonner";
import "./globals.css";
@@ -35,7 +36,7 @@ export default function RootLayout({
</head>
<body className="subpixel-antialiased" suppressHydrationWarning>
<ThemeProvider attribute="class" defaultTheme="light">
{children}
<Suspense>{children}</Suspense>
<Toaster position="top-right" />
</ThemeProvider>
</body>

View File

@@ -1,4 +1,4 @@
import { unstable_cache } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import { fetchDashboardAccounts } from "./accounts-queries";
import { fetchDashboardCategoryOverview } from "./category-overview-queries";
import { fetchDashboardCurrentPeriodOverview } from "./current-period-overview-queries";
@@ -51,18 +51,14 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
/**
* Cached dashboard data fetcher.
* Uses unstable_cache with tags for revalidation on mutations.
* Uses "use cache" with tags for revalidation on mutations.
* Cache is keyed by userId + period, and invalidated via user-scoped tags.
*/
export function fetchDashboardData(userId: string, period: string) {
return unstable_cache(
() => fetchDashboardDataInternal(userId, period),
[`dashboard-${userId}-${period}`],
{
tags: [`dashboard-${userId}`],
revalidate: 60,
},
)();
export async function fetchDashboardData(userId: string, period: string) {
"use cache";
cacheTag(`dashboard-${userId}`);
cacheLife({ revalidate: 3 });
return fetchDashboardDataInternal(userId, period);
}
export type DashboardData = Awaited<ReturnType<typeof fetchDashboardData>>;

View File

@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm";
import { unstable_cache } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import { payers } from "@/db/schema";
import { fetchPendingInboxCount } from "@/features/inbox/queries";
import { db } from "@/shared/lib/db";
@@ -53,15 +53,9 @@ async function fetchDashboardNavbarDataInternal(
};
}
export function fetchDashboardNavbarData(userId: string) {
const currentPeriod = getBusinessDateString().slice(0, 7);
return unstable_cache(
() => fetchDashboardNavbarDataInternal(userId),
[`dashboard-navbar-${userId}-${currentPeriod}`],
{
tags: [`dashboard-${userId}`],
revalidate: 60,
},
)();
export async function fetchDashboardNavbarData(userId: string) {
"use cache";
cacheTag(`dashboard-${userId}`);
cacheLife({ revalidate: 3 });
return fetchDashboardNavbarDataInternal(userId);
}

View File

@@ -1,4 +1,4 @@
import { unstable_cache } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import { fetchDashboardData } from "@/features/dashboard/fetch-dashboard-data";
import { fetchUserDashboardPreferences } from "@/features/dashboard/preferences-queries";
import {
@@ -52,15 +52,11 @@ async function fetchDashboardQuickActionOptionsInternal(
};
}
export function fetchDashboardQuickActionOptions(userId: string) {
return unstable_cache(
() => fetchDashboardQuickActionOptionsInternal(userId),
[`dashboard-quick-actions-${userId}`],
{
tags: [`dashboard-${userId}`],
revalidate: 60,
},
)();
export async function fetchDashboardQuickActionOptions(userId: string) {
"use cache";
cacheTag(`dashboard-${userId}`);
cacheLife({ revalidate: 3 });
return fetchDashboardQuickActionOptionsInternal(userId);
}
export async function fetchDashboardPageData(userId: string, period: string) {

View File

@@ -1,4 +1,5 @@
import { eq } from "drizzle-orm";
import { cacheLife, cacheTag } from "next/cache";
import type { WidgetPreferences } from "@/features/dashboard/widgets/actions";
import { db, schema } from "@/shared/lib/db";
@@ -9,6 +10,10 @@ export interface UserDashboardPreferences {
export async function fetchUserDashboardPreferences(
userId: string,
): Promise<UserDashboardPreferences> {
"use cache";
cacheTag(`dashboard-${userId}`);
cacheLife({ revalidate: 3 });
const result = await db
.select({
dashboardWidgets: schema.userPreferences.dashboardWidgets,

View File

@@ -1,6 +1,6 @@
import { getDay } from "date-fns";
import { and, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
import { unstable_cache } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import {
budgets,
cards,
@@ -481,13 +481,9 @@ async function aggregateMonthDataInternal(userId: string, period: string) {
};
}
export function aggregateMonthData(userId: string, period: string) {
return unstable_cache(
() => aggregateMonthDataInternal(userId, period),
[`insights-aggregate-${userId}-${period}`],
{
tags: [`dashboard-${userId}`],
revalidate: 60,
},
)();
export async function aggregateMonthData(userId: string, period: string) {
"use cache";
cacheTag(`dashboard-${userId}`);
cacheLife({ revalidate: 3 });
return aggregateMonthDataInternal(userId, period);
}