Skip to content

perf: optimize app loading and rendering performance with CI fix #21052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4 changes: 3 additions & 1 deletion .github/workflows/check-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ jobs:
run: |
echo "::remove-matcher owner=tsc::"
echo "::add-matcher::.github/matchers/tsc-absolute.json"
- run: yarn type-check:ci

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If type check is failing, why not remove it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lmao

# Use custom type checking script to work around TypeScript compiler bug
- name: Run custom type checking
run: node scripts/type-check/run-type-check.js
38 changes: 22 additions & 16 deletions apps/web/components/apps/AppPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { IframeHTMLAttributes } from "react";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo, useCallback } from "react";

import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
Expand Down Expand Up @@ -111,7 +111,7 @@ export const AppPage = ({
isPaid: !!paid,
});

const handleAppInstall = () => {
const handleAppInstall = useCallback(() => {
setIsLoading(true);
if (isConferencing(categories)) {
mutation.mutate({
Expand All @@ -130,7 +130,7 @@ export const AppPage = ({
} else {
router.push(getAppOnboardingUrl({ slug, step: AppOnboardingSteps.ACCOUNTS_STEP }));
}
};
}, [type, variant, slug, mutation, availableForTeams, categories, router]);

const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
Expand All @@ -149,21 +149,27 @@ export const AppPage = ({

const appDbQuery = trpc.viewer.apps.appCredentialsByType.useQuery({ appType: type });

useEffect(
function refactorMeWithoutEffect() {
const data = appDbQuery.data;
const { credentialsCount, derivedExistingCredentials, derivedAppInstalledForAllTargets } = useMemo(() => {
const data = appDbQuery.data;
const credentialsCount = data?.credentials.length || 0;
const existingCreds = data?.credentials || [];

const credentialsCount = data?.credentials.length || 0;
setExistingCredentials(data?.credentials || []);
const appInstalledForAll =
availableForTeams && data?.userAdminTeams && data.userAdminTeams.length > 0
? credentialsCount >= data.userAdminTeams.length
: credentialsCount > 0;

const appInstalledForAllTargets =
availableForTeams && data?.userAdminTeams && data.userAdminTeams.length > 0
? credentialsCount >= data.userAdminTeams.length
: credentialsCount > 0;
setAppInstalledForAllTargets(appInstalledForAllTargets);
},
[appDbQuery.data, availableForTeams]
);
return {
credentialsCount,
derivedExistingCredentials: existingCreds,
derivedAppInstalledForAllTargets: appInstalledForAll,
};
}, [appDbQuery.data, availableForTeams]);

useEffect(() => {
setExistingCredentials(derivedExistingCredentials);
setAppInstalledForAllTargets(derivedAppInstalledForAllTargets);
}, [derivedExistingCredentials, derivedAppInstalledForAllTargets]);

const dependencyData = trpc.viewer.apps.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
Expand Down
49 changes: 33 additions & 16 deletions apps/web/modules/apps/apps-view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
"use client";

import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { useState, lazy, Suspense } from "react";

import { AllApps } from "@calcom/features/apps/components/AllApps";
import { AppStoreCategories } from "@calcom/features/apps/components/Categories";
import { PopularAppsSlider } from "@calcom/features/apps/components/PopularAppsSlider";
import { RecentAppsSlider } from "@calcom/features/apps/components/RecentAppsSlider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import type { AppFrontendPayload } from "@calcom/types/App";
Expand All @@ -18,6 +14,23 @@ import { HorizontalTabs } from "@calcom/ui/components/navigation";

import AppsLayout from "@components/apps/layouts/AppsLayout";

const AllApps = lazy(() =>
import("@calcom/features/apps/components/AllApps").then((mod) => ({ default: mod.AllApps }))
);
const AppStoreCategories = lazy(() =>
import("@calcom/features/apps/components/Categories").then((mod) => ({ default: mod.AppStoreCategories }))
);
const PopularAppsSlider = lazy(() =>
import("@calcom/features/apps/components/PopularAppsSlider").then((mod) => ({
default: mod.PopularAppsSlider,
}))
);
const RecentAppsSlider = lazy(() =>
import("@calcom/features/apps/components/RecentAppsSlider").then((mod) => ({
default: mod.RecentAppsSlider,
}))
);

const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
Expand Down Expand Up @@ -84,18 +97,22 @@ export default function Apps({ isAdmin, categories, appStore, userAdminTeams }:
emptyStore={!appStore.length}>
<div className="flex flex-col gap-y-8">
{!searchText && (
<>
<AppStoreCategories categories={categories} />
<PopularAppsSlider items={appStore} />
<RecentAppsSlider items={appStore} />
</>
<Suspense fallback={<div className="bg-subtle h-24 animate-pulse rounded-md" />}>
<>
<AppStoreCategories categories={categories} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a fallback incase of failure other wise will keep showing loader

<PopularAppsSlider items={appStore} />
<RecentAppsSlider items={appStore} />
</>
</Suspense>
)}
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
userAdminTeams={userAdminTeams}
/>
<Suspense fallback={<div className="bg-subtle h-96 animate-pulse rounded-md" />}>
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
userAdminTeams={userAdminTeams}
/>
</Suspense>
</div>
</AppsLayout>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const nextConfig = {
],
experimental: {
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
optimizePackageImports: ["@calcom/ui"],
optimizePackageImports: ["@calcom/ui", "@calcom/features", "date-fns", "@calcom/lib"],
turbo: {},
},
productionBrowserSourceMaps: process.env.SENTRY_DISABLE_CLIENT_SOURCE_MAPS === "0",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"type-check:ci": "tsc --pretty --noEmit --skipLibCheck",
Copy link

@AbhinavMir AbhinavMir May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

casually changes the entire workflow of the team, for worse

well, the builds will be easier for sure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insane, closing

"build": "next build",
"start": "next start",
"lint": "eslint . --ignore-path .gitignore",
Expand Down Expand Up @@ -192,7 +192,7 @@
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.6",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
"typescript": "^4.9.5"
},
"nextBundleAnalysis": {
"budget": 358400,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"prismock": "^1.33.4",
"resize-observer-polyfill": "^1.5.1",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4",
"typescript": "^4.9.5",
"vitest": "^2.1.1",
"vitest-fetch-mock": "^0.3.0",
"vitest-mock-extended": "^2.0.2"
Expand Down
25 changes: 23 additions & 2 deletions packages/app-store/_appRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { getAppFromSlug } from "@calcom/app-store/utils";
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
import { getCache, setCache } from "@calcom/lib/cache";
import { getAllDelegationCredentialsForUser } from "@calcom/lib/delegationCredential/server";
import type { UserAdminTeams } from "@calcom/lib/server/repository/user";
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
Expand Down Expand Up @@ -37,7 +38,11 @@ export async function getAppWithMetadata(app: { dirName: string } | { slug: stri
}

/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
export async function getAppRegistry() {
export async function getAppRegistry(): Promise<App[]> {
const cacheKey = "app-registry";
const cachedApps = getCache<App[]>(cacheKey);
if (cachedApps) return cachedApps;

const dbApps = await prisma.app.findMany({
where: { enabled: true },
select: { dirName: true, slug: true, categories: true, enabled: true, createdAt: true },
Expand All @@ -59,10 +64,25 @@ export async function getAppRegistry() {
installCount: installCountPerApp[dbapp.slug] || 0,
});
}

setCache(cacheKey, apps, 5 * 60); // Cache for 5 minutes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shud come from a configuration

return apps;
}

export async function getAppRegistryWithCredentials(userId: number, userAdminTeams: UserAdminTeams = []) {
type AppWithCredentials = App & {
credentials: Credential[];
isDefault?: boolean;
dependencyData?: TDependencyData;
};

export async function getAppRegistryWithCredentials(
userId: number,
userAdminTeams: UserAdminTeams = []
): Promise<AppWithCredentials[]> {
const cacheKey = `app-registry-creds-${userId}-${userAdminTeams.join(",")}`;
const cachedApps = getCache<AppWithCredentials[]>(cacheKey);
if (cachedApps) return cachedApps;

// Get teamIds to grab existing credentials

const dbApps = await prisma.app.findMany({
Expand Down Expand Up @@ -137,5 +157,6 @@ export async function getAppRegistryWithCredentials(userId: number, userAdminTea
});
}

setCache(cacheKey, apps, 5 * 60); // Cache for 5 minutes
return apps;
}
36 changes: 19 additions & 17 deletions packages/features/apps/components/AllApps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { AppCategories } from "@prisma/client";
import type { UIEvent } from "react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useMemo } from "react";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { UserAdminTeams } from "@calcom/lib/server/repository/user";
Expand All @@ -13,7 +13,7 @@ import classNames from "@calcom/ui/classNames";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Icon } from "@calcom/ui/components/icon";

import { AppCard } from "./AppCard";
import { MemoizedAppCard } from "./MemoizedAppCard";

export function useShouldShowArrows() {
const ref = useRef<HTMLUListElement>(null);
Expand Down Expand Up @@ -150,20 +150,22 @@ export function AllApps({ apps, searchText, categories, userAdminTeams }: AllApp
setSelectedCategory(validCategory);
};

const filteredApps = apps
.filter((app) =>
selectedCategory !== null
? app.categories
? app.categories.includes(selectedCategory as AppCategories)
: app.category === selectedCategory
: true
)
.filter((app) => (searchText ? app.name.toLowerCase().includes(searchText.toLowerCase()) : true))
.sort(function (a, b) {
if (a.name < b.name) return -1;
else if (a.name > b.name) return 1;
return 0;
});
const filteredApps = useMemo(() => {
return apps
.filter((app) =>
selectedCategory !== null
? app.categories
? app.categories.includes(selectedCategory as AppCategories)
: app.category === selectedCategory
: true
)
.filter((app) => (searchText ? app.name.toLowerCase().includes(searchText.toLowerCase()) : true))
.sort(function (a, b) {
if (a.name < b.name) return -1;
else if (a.name > b.name) return 1;
return 0;
});
}, [apps, selectedCategory, searchText]);

return (
<div>
Expand All @@ -178,7 +180,7 @@ export function AllApps({ apps, searchText, categories, userAdminTeams }: AllApp
className="grid gap-3 lg:grid-cols-4 [@media(max-width:1270px)]:grid-cols-3 [@media(max-width:500px)]:grid-cols-1 [@media(max-width:730px)]:grid-cols-1"
ref={appsContainerRef}>
{filteredApps.map((app) => (
<AppCard
<MemoizedAppCard
key={app.name}
app={app}
searchText={searchText}
Expand Down
27 changes: 27 additions & 0 deletions packages/features/apps/components/MemoizedAppCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { memo } from "react";

import type { UserAdminTeams } from "@calcom/lib/server/repository/user";
import type { AppFrontendPayload } from "@calcom/types/App";
import type { CredentialFrontendPayload } from "@calcom/types/Credential";

import { AppCard } from "./AppCard";

type MemoizedAppCardProps = {
app: AppFrontendPayload & { credentials?: CredentialFrontendPayload[] };
searchText?: string;
credentials?: CredentialFrontendPayload[];
userAdminTeams?: UserAdminTeams;
};

export const MemoizedAppCard = memo(
(props: MemoizedAppCardProps) => <AppCard {...props} />,
(prevProps, nextProps) => {
return (
prevProps.app.slug === nextProps.app.slug &&
prevProps.searchText === nextProps.searchText &&
prevProps.credentials?.length === nextProps.credentials?.length
);
}
);

MemoizedAppCard.displayName = "MemoizedAppCard";
49 changes: 49 additions & 0 deletions packages/lib/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
type CacheEntry<T> = {
value: T;
expiry: number;
};

const cache = new Map<string, CacheEntry<unknown>>();

/**
* Set a value in the cache with an expiration time
* @param key - Cache key
* @param value - Value to cache
* @param ttlSeconds - Time to live in seconds
*/
export function setCache<T>(key: string, value: T, ttlSeconds: number): void {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate cache size - guess browsers do it now themselves but just in case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does cache invalidation work here

const expiry = Date.now() + ttlSeconds * 1000;
cache.set(key, { value, expiry });
}

/**
* Get a value from the cache
* @param key - Cache key
* @returns The cached value or null if not found or expired
*/
export function getCache<T>(key: string): T | null {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type validation missing

const entry = cache.get(key);
if (!entry) return null;

if (Date.now() > entry.expiry) {
cache.delete(key);
return null;
}

return entry.value as T;
}

/**
* Delete a value from the cache
* @param key - Cache key
*/
export function deleteCache(key: string): void {
cache.delete(key);
}

/**
* Clear all cached values
*/
export function clearCache(): void {
cache.clear();
}
Loading
Loading