Skip to content

Commit 64d5bfc

Browse files
committed
feat: Introduce ResourcePreloader component and default configuration to optimize resource loading in App.tsx.
1 parent afff279 commit 64d5bfc

File tree

4 files changed

+273
-0
lines changed

4 files changed

+273
-0
lines changed

src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { QueryClient, QueryClientProvider } from "react-query";
44
import { Link, Route, Routes } from "react-router";
55
import { styled } from "styled-components";
66

7+
import { ResourcePreloader } from "@components/common/ResourcePreloader";
78
import { Footer } from "@components/Footer/Footer";
89
import { Loading } from "@components/Loading/Loading";
910
import { Navigation } from "@components/Navigation/Navigation";
1011
import { ScrollToTop } from "@components/ScrollToTop/ScrollToTop";
12+
import { DEFAULT_PRELOAD_CONFIG } from "@config/preloadConfig";
1113
import { getAllRoutes } from "@config/routeConfig";
1214
import { ROUTE_COOKIES } from "@constants/routes";
1315
import { Color } from "@styles/colors";
@@ -52,6 +54,9 @@ const App: FC<React.PropsWithChildren<unknown>> = () => {
5254

5355
return (
5456
<StyledAppWrapper className="AppWrapperAll">
57+
{/* Preload critical resources for better performance */}
58+
<ResourcePreloader {...DEFAULT_PRELOAD_CONFIG} />
59+
5560
<QueryClientProvider client={queryClient}>
5661
<ScrollToTop />
5762
<Navigation />
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, expect, it, beforeEach, afterEach } from "vitest";
3+
4+
import { ResourcePreloader } from "./ResourcePreloader";
5+
6+
describe("ResourcePreloader", () => {
7+
beforeEach(() => {
8+
// Clear any existing link tags before each test
9+
document.head.innerHTML = "";
10+
});
11+
12+
afterEach(() => {
13+
// Clean up after each test
14+
document.head.innerHTML = "";
15+
});
16+
17+
it("should add DNS prefetch links", () => {
18+
render(
19+
<ResourcePreloader
20+
dnsPrefetch={["https://example.com", "https://api.example.com"]}
21+
/>,
22+
);
23+
24+
const dnsPrefetchLinks = document.querySelectorAll(
25+
'link[rel="dns-prefetch"]',
26+
);
27+
expect(dnsPrefetchLinks).toHaveLength(2);
28+
expect(dnsPrefetchLinks[0].getAttribute("href")).toBe(
29+
"https://example.com",
30+
);
31+
expect(dnsPrefetchLinks[1].getAttribute("href")).toBe(
32+
"https://api.example.com",
33+
);
34+
});
35+
36+
it("should add preconnect links", () => {
37+
render(
38+
<ResourcePreloader
39+
preconnect={["https://fonts.googleapis.com", "https://cdn.example.com"]}
40+
/>,
41+
);
42+
43+
const preconnectLinks = document.querySelectorAll('link[rel="preconnect"]');
44+
expect(preconnectLinks).toHaveLength(2);
45+
expect(preconnectLinks[0].getAttribute("href")).toBe(
46+
"https://fonts.googleapis.com",
47+
);
48+
expect(preconnectLinks[0].getAttribute("crossorigin")).toBe("anonymous");
49+
});
50+
51+
it("should add preload links for fonts", () => {
52+
render(
53+
<ResourcePreloader
54+
preload={[
55+
{
56+
href: "/fonts/inter.woff2",
57+
as: "font",
58+
type: "font/woff2",
59+
crossOrigin: "anonymous",
60+
},
61+
]}
62+
/>,
63+
);
64+
65+
const preloadLinks = document.querySelectorAll('link[rel="preload"]');
66+
expect(preloadLinks).toHaveLength(1);
67+
expect(preloadLinks[0].getAttribute("href")).toBe("/fonts/inter.woff2");
68+
expect(preloadLinks[0].getAttribute("as")).toBe("font");
69+
expect(preloadLinks[0].getAttribute("type")).toBe("font/woff2");
70+
expect(preloadLinks[0].getAttribute("crossorigin")).toBe("anonymous");
71+
});
72+
73+
it("should add preload links for images", () => {
74+
render(
75+
<ResourcePreloader
76+
preload={[
77+
{
78+
href: "/images/hero.jpg",
79+
as: "image",
80+
},
81+
]}
82+
/>,
83+
);
84+
85+
const preloadLinks = document.querySelectorAll('link[rel="preload"]');
86+
expect(preloadLinks).toHaveLength(1);
87+
expect(preloadLinks[0].getAttribute("href")).toBe("/images/hero.jpg");
88+
expect(preloadLinks[0].getAttribute("as")).toBe("image");
89+
});
90+
91+
it("should add preload links for scripts", () => {
92+
render(
93+
<ResourcePreloader
94+
preload={[
95+
{
96+
href: "/scripts/analytics.js",
97+
as: "script",
98+
},
99+
]}
100+
/>,
101+
);
102+
103+
const preloadLinks = document.querySelectorAll('link[rel="preload"]');
104+
expect(preloadLinks).toHaveLength(1);
105+
expect(preloadLinks[0].getAttribute("href")).toBe("/scripts/analytics.js");
106+
expect(preloadLinks[0].getAttribute("as")).toBe("script");
107+
});
108+
109+
it("should handle all resource types together", () => {
110+
render(
111+
<ResourcePreloader
112+
dnsPrefetch={["https://example.com"]}
113+
preconnect={["https://fonts.googleapis.com"]}
114+
preload={[
115+
{ href: "/fonts/inter.woff2", as: "font", type: "font/woff2" },
116+
{ href: "/images/logo.png", as: "image" },
117+
]}
118+
/>,
119+
);
120+
121+
const dnsPrefetchLinks = document.querySelectorAll(
122+
'link[rel="dns-prefetch"]',
123+
);
124+
const preconnectLinks = document.querySelectorAll('link[rel="preconnect"]');
125+
const preloadLinks = document.querySelectorAll('link[rel="preload"]');
126+
127+
expect(dnsPrefetchLinks).toHaveLength(1);
128+
expect(preconnectLinks).toHaveLength(1);
129+
expect(preloadLinks).toHaveLength(2);
130+
});
131+
132+
it("should handle empty arrays", () => {
133+
render(<ResourcePreloader dnsPrefetch={[]} preconnect={[]} preload={[]} />);
134+
135+
const allLinks = document.querySelectorAll("link");
136+
expect(allLinks).toHaveLength(0);
137+
});
138+
139+
it("should not render any visible content", () => {
140+
const { container } = render(
141+
<ResourcePreloader dnsPrefetch={["https://example.com"]} />,
142+
);
143+
144+
expect(container.textContent).toBe("");
145+
});
146+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useEffect } from "react";
2+
3+
/**
4+
* Resource preloading configuration using React 19's native APIs.
5+
* This component preloads critical resources to improve page load performance.
6+
*/
7+
8+
export interface ResourcePreloadConfig {
9+
/** DNS prefetching for external domains */
10+
dnsPrefetch?: string[];
11+
/** Preconnect to external origins */
12+
preconnect?: string[];
13+
/** Preload critical resources */
14+
preload?: Array<{
15+
href: string;
16+
as: "font" | "image" | "script" | "style" | "fetch";
17+
type?: string;
18+
crossOrigin?: "anonymous" | "use-credentials";
19+
}>;
20+
}
21+
22+
/**
23+
* ResourcePreloader component that uses React 19's native resource preloading APIs.
24+
*
25+
* @example
26+
* ```tsx
27+
* <ResourcePreloader
28+
* dnsPrefetch={["https://sessionize.com"]}
29+
* preconnect={["https://fonts.googleapis.com"]}
30+
* preload={[
31+
* { href: "/fonts/inter.woff2", as: "font", type: "font/woff2", crossOrigin: "anonymous" }
32+
* ]}
33+
* />
34+
* ```
35+
*/
36+
export const ResourcePreloader: React.FC<ResourcePreloadConfig> = ({
37+
dnsPrefetch = [],
38+
preconnect = [],
39+
preload = [],
40+
}) => {
41+
useEffect(() => {
42+
// DNS Prefetch - resolve DNS early for external domains
43+
dnsPrefetch.forEach((href) => {
44+
const link = document.createElement("link");
45+
link.rel = "dns-prefetch";
46+
link.href = href;
47+
document.head.appendChild(link);
48+
});
49+
50+
// Preconnect - establish early connections to external origins
51+
preconnect.forEach((href) => {
52+
const link = document.createElement("link");
53+
link.rel = "preconnect";
54+
link.href = href;
55+
link.setAttribute("crossorigin", "anonymous");
56+
document.head.appendChild(link);
57+
});
58+
59+
// Preload - load critical resources early
60+
preload.forEach(({ href, as, type, crossOrigin }) => {
61+
const link = document.createElement("link");
62+
link.rel = "preload";
63+
link.href = href;
64+
link.setAttribute("as", as);
65+
if (type) link.setAttribute("type", type);
66+
if (crossOrigin) link.setAttribute("crossorigin", crossOrigin);
67+
document.head.appendChild(link);
68+
});
69+
}, [dnsPrefetch, preconnect, preload]);
70+
71+
return null; // This component doesn't render anything
72+
};

src/config/preloadConfig.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { ResourcePreloadConfig } from "@components/common/ResourcePreloader";
2+
3+
/**
4+
* Default preload configuration for the DevBcn website.
5+
* Includes common resources that should be preloaded on every page.
6+
*/
7+
export const DEFAULT_PRELOAD_CONFIG: ResourcePreloadConfig = {
8+
// DNS Prefetch for external APIs
9+
dnsPrefetch: [
10+
"https://sessionize.com",
11+
"https://www.google-analytics.com",
12+
"https://fonts.googleapis.com",
13+
"https://fonts.gstatic.com",
14+
],
15+
16+
// Preconnect to critical external origins
17+
preconnect: [
18+
"https://sessionize.com",
19+
"https://fonts.googleapis.com",
20+
"https://fonts.gstatic.com",
21+
],
22+
23+
// Preload critical fonts
24+
preload: [
25+
{
26+
href: "/fonts/DejaVu Sans Bold.ttf",
27+
as: "font",
28+
type: "font/ttf",
29+
crossOrigin: "anonymous",
30+
},
31+
{
32+
href: "/fonts/DejaVu Sans Condensed Bold.ttf",
33+
as: "font",
34+
type: "font/ttf",
35+
crossOrigin: "anonymous",
36+
},
37+
{
38+
href: "/fonts/DejaVu Sans ExtraLight.ttf",
39+
as: "font",
40+
type: "font/ttf",
41+
crossOrigin: "anonymous",
42+
},
43+
{
44+
href: "/fonts/Square 721 Regular.otf",
45+
as: "font",
46+
type: "font/otf",
47+
crossOrigin: "anonymous",
48+
},
49+
],
50+
};

0 commit comments

Comments
 (0)