JavaScript Minimaliste : Comment Vaincre le Framework Fatigue et Améliorer la Performance en 2025
Salut HaWkers, vous êtes-vous déjà senti épuisé par la quantité absurde de frameworks, bibliothèques et outils dans l'écosystème JavaScript ? Vous n'êtes pas seul. Le framework fatigue est réel, et une nouvelle tendance émerge en 2025 : JavaScript minimaliste.
Les développeurs et entreprises réalisent que moins de code = meilleure performance = meilleur SEO = plus d'argent. Explorons comment simplifier votre stack sans perdre en productivité.
Le Problème : Le Code Surchargé Tue la Performance
Réalité en 2025 : Bundle Size Hors de Contrôle
// Projet JavaScript typique en 2023-2024
const typicalProject2024 = {
react: "18.2.0", // 44kb gzipped
reactDom: "18.2.0", // 132kb gzipped
redux: "4.2.1", // 3kb
reactRedux: "8.1.3", // 16kb
reduxToolkit: "1.9.7", // 48kb
reactRouter: "6.20.1", // 14kb
axios: "1.6.2", // 13kb
lodash: "4.17.21", // 24kb (si tout importé)
momentJs: "2.29.4", // 72kb (!) - deprecated mais encore utilisé
materialUI: "5.14.20", // 400kb+ (!)
totalBundle: "~800kb gzipped", // 2.5-3MB non compressé
loadTime: "4-7s sur 3G",
lighthouseScore: "45-65 (mauvais)",
seoImpact: "Pénalisé par Google (Core Web Vitals)",
};
// Projet minimaliste en 2025
const minimalistProject2025 = {
react: "18.3.0", // Toujours 44kb (nécessaire)
reactDom: "18.3.0", // Toujours 132kb (nécessaire)
zustand: "4.5.0", // 1.2kb (!!) - remplace Redux
tinyRouter: "Custom", // 2kb - remplace React Router
nativeFetch: "Built-in", // 0kb - remplace Axios
nativeMethods: "Built-in", // 0kb - remplace Lodash
dayjs: "1.11.10", // 2kb - remplace Moment.js
tailwindCSS: "3.4.0", // 10kb runtime - remplace MUI
totalBundle: "~200kb gzipped", // 600kb non compressé
loadTime: "1-2s sur 3G",
lighthouseScore: "90-100 (excellent)",
seoImpact: "Favorisé par Google",
savings: {
bundleSize: "-75%",
loadTime: "-60%",
maintenanceCost: "-50%",
},
};
Renaissance du Vanilla JS : Retour aux Fondamentaux
Quand Vanilla JS Suffit
// 1. Manipulation du DOM (pas besoin de jQuery en 2025)
// ❌ Avant (jQuery) : 30kb de surcharge
$("#myButton").on("click", function () {
$(this).addClass("active");
$(".modal").fadeIn();
});
// ✅ Maintenant (Vanilla JS) : 0kb de surcharge
document.getElementById("myButton").addEventListener("click", function () {
this.classList.add("active");
// Fade in avec CSS + JS
const modal = document.querySelector(".modal");
modal.style.display = "block";
modal.style.opacity = "0";
requestAnimationFrame(() => {
modal.style.transition = "opacity 0.3s";
modal.style.opacity = "1";
});
});
// 2. Requêtes HTTP (Fetch API native)
// ❌ Avant (Axios) : 13kb
import axios from "axios";
const { data } = await axios.get("/api/users", {
params: { role: "admin" },
timeout: 5000,
});
// ✅ Maintenant (Fetch natif) : 0kb
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch("/api/users?role=admin", {
signal: controller.signal,
});
clearTimeout(timeout);
const data = await response.json();
// Wrapper réutilisable
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`Erreur HTTP ! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
// 3. Manipulation Array/Object (sans Lodash)
// ❌ Avant (Lodash) : 24kb
import _ from "lodash";
const uniqueUsers = _.uniqBy(users, "id");
const grouped = _.groupBy(products, "category");
const debounced = _.debounce(handleSearch, 300);
// ✅ Maintenant (JS Natif) : 0kb
const uniqueUsers = [...new Map(users.map((u) => [u.id, u])).values()];
const grouped = products.reduce((acc, product) => {
const category = product.category;
if (!acc[category]) acc[category] = [];
acc[category].push(product);
return acc;
}, {});
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const debouncedSearch = debounce(handleSearch, 300);Web Components : L'Avenir du JavaScript Minimaliste ?
// Web Components natifs (zéro dépendances)
class TodoList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.todos = [];
}
connectedCallback() {
this.render();
this.attachEventListeners();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui;
}
.todo-item {
display: flex;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-item.completed {
opacity: 0.5;
text-decoration: line-through;
}
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
<div class="todo-app">
<input
type="text"
id="newTodo"
placeholder="Ajouter une tâche..."
/>
<button id="addBtn">Ajouter</button>
<div id="todoList">
${this.todos
.map(
(todo) => `
<div class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input type="checkbox" ${todo.completed ? "checked" : ""} />
<span>${todo.text}</span>
<button class="delete-btn">Supprimer</button>
</div>
`
)
.join("")}
</div>
</div>
`;
}
attachEventListeners() {
const addBtn = this.shadowRoot.getElementById("addBtn");
const input = this.shadowRoot.getElementById("newTodo");
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (text) {
this.addTodo(text);
input.value = "";
}
});
// Délégation d'événements pour checkboxes et boutons delete
this.shadowRoot.addEventListener("change", (e) => {
if (e.target.type === "checkbox") {
const id = e.target.closest(".todo-item").dataset.id;
this.toggleTodo(id);
}
});
this.shadowRoot.addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
const id = e.target.closest(".todo-item").dataset.id;
this.deleteTodo(id);
}
});
}
addTodo(text) {
this.todos.push({
id: Date.now().toString(),
text,
completed: false,
});
this.render();
this.attachEventListeners();
}
toggleTodo(id) {
const todo = this.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.render();
this.attachEventListeners();
}
}
deleteTodo(id) {
this.todos = this.todos.filter((t) => t.id !== id);
this.render();
this.attachEventListeners();
}
}
// Enregistrer le composant
customElements.define("todo-list", TodoList);
// Utilisation en HTML (zéro étape de build !)
// <todo-list></todo-list>
// Taille du bundle : ~0kb (natif du navigateur)
// Compatibilité : Tous les navigateurs modernes (98%+ support)
Zustand vs Redux : La Révolution Minimaliste
Pourquoi Zustand Domine en 2025
// ❌ Redux Toolkit (encore verbeux même "simplifié")
// Fichiers : store.ts, userSlice.ts, productSlice.ts, hooks.ts, etc.
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice";
import productReducer from "./productSlice";
export const store = configureStore({
reducer: {
user: userReducer,
products: productReducer,
},
});
// userSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUser = createAsyncThunk("user/fetchUser", async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
const userSlice = createSlice({
name: "user",
initialState: { data: null, loading: false, error: null },
reducers: {
setUser: (state, action) => {
state.data = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export const { setUser } = userSlice.actions;
export default userSlice.reducer;
// Composant
import { useDispatch, useSelector } from "react-redux";
function UserProfile() {
const dispatch = useDispatch();
const user = useSelector((state) => state.user.data);
const loading = useSelector((state) => state.user.loading);
useEffect(() => {
dispatch(fetchUser("123"));
}, []);
// ...
}
// Total : ~100 lignes de code, multiples fichiers, 67kb bundle
// ✅ Zustand (minimaliste et puissant)
import { create } from "zustand";
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: (id: string) => Promise<void>;
setUser: (user: User) => void;
}
export const useUserStore = create<UserState>((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
setUser: (user) => set({ user }),
}));
// Composant (BEAUCOUP plus simple)
function UserProfile() {
const { user, loading, fetchUser } = useUserStore();
useEffect(() => {
fetchUser("123");
}, []);
if (loading) return <Spinner />;
return <div>{user?.name}</div>;
}
// Total : ~30 lignes, 1 fichier, 1.2kb bundle (-98% !)Zustand : Cas d'Usage Avancés
// Persistance automatique
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const useCartStore = create(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
// Valeur calculée (similaire aux getters)
get total() {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
}),
{
name: "cart-storage", // clé localStorage
partialize: (state) => ({ items: state.items }), // Sauvegarde uniquement items
}
)
);
// Pattern slices (organisation avancée)
const createUserSlice = (set) => ({
user: null,
login: (credentials) => {
/* ... */
},
logout: () => set({ user: null }),
});
const createProductSlice = (set) => ({
products: [],
fetchProducts: async () => {
/* ... */
},
});
export const useStore = create((...a) => ({
...createUserSlice(...a),
...createProductSlice(...a),
}));
// Middleware personnalisé (DevTools, logging, etc)
import { devtools, subscribeWithSelector } from "zustand/middleware";
export const useStore = create(
devtools(
subscribeWithSelector((set) => ({
// State ici
})),
{ name: "MyAppStore" }
)
);
// S'abonner aux changements spécifiques
useStore.subscribe(
(state) => state.user,
(user, prevUser) => {
console.log("Utilisateur changé :", { user, prevUser });
// Analytics, effets secondaires, etc
}
);
Jotai : Le Minimaliste Atomique
// Jotai : Encore plus minimaliste que Zustand (approche atoms)
import { atom, useAtom } from "jotai";
// 1. Atoms basiques
const countAtom = atom(0);
const userAtom = atom(null);
// Composant
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<button onClick={() => setCount((c) => c + 1)}>Compteur : {count}</button>
);
}
// 2. Atoms dérivés (valeurs calculées)
const todosAtom = atom([]);
const completedTodosAtom = atom((get) =>
get(todosAtom).filter((t) => t.completed)
);
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: get(completedTodosAtom).length,
pending: todos.filter((t) => !t.completed).length,
};
});
// Composant
function TodoStats() {
const [stats] = useAtom(todoStatsAtom);
return (
<div>
Total : {stats.total} | Terminées : {stats.completed} | En attente :{" "}
{stats.pending}
</div>
);
}
// 3. Atoms async
const userIdAtom = atom("123");
const userAtom = atom(async (get) => {
const userId = get(userIdAtom);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
// Composant (Suspense automatique !)
function UserProfile() {
const [user] = useAtom(userAtom);
// user est une Promise, mais Suspense gère ça
return <div>{user.name}</div>;
}
// Wrapper avec Suspense
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
);
}
// 4. Atoms write-only (actions)
const todosAtom = atom([]);
const addTodoAtom = atom(
null, // fonction read (null = write-only)
(get, set, newTodo) => {
set(todosAtom, [...get(todosAtom), newTodo]);
}
);
const removeTodoAtom = atom(null, (get, set, id) => {
set(
todosAtom,
get(todosAtom).filter((t) => t.id !== id)
);
});
// Composant
function AddTodo() {
const [, addTodo] = useAtom(addTodoAtom);
return (
<button onClick={() => addTodo({ id: Date.now(), text: "Nouvelle tâche" })}>
Ajouter
</button>
);
}
// Bundle : 3kb (!!!) - Plus petit que Zustand
// Performance : Excellente (re-renders uniquement quand l'atom spécifique change)Zustand vs Jotai : Quand Utiliser Chacun
const stateManagementGuide = {
useZustand: {
when: [
"Store global centralisé",
"Nombreuses actions complexes",
"Familiarité avec Redux",
"Persistance localStorage",
"DevTools essentiel",
],
examples: ["E-commerce (panier)", "État auth", "Thème/paramètres"],
size: "1.2kb",
},
useJotai: {
when: [
"État granulaire (nombreux petits atoms)",
"État dérivé complexe",
"Suspense/async natif",
"Performance critique (éviter re-renders)",
"Taille bundle critique",
],
examples: ["Formulaires complexes", "Dashboards temps réel", "État de jeu"],
size: "3kb",
},
useReact: {
when: ["État local simple", "Composant isolé", "Prototypes rapides"],
examples: ["Toggles", "Inputs formulaire", "Modals"],
size: "0kb (built-in)",
},
};
Performance : Métriques Réelles
// Benchmark : Application e-commerce (10k produits)
const performanceComparison = {
heavyStack: {
// React + Redux Toolkit + MUI + Lodash + Moment
bundleSize: "850kb gzipped",
fcp: "2.8s", // First Contentful Paint
lcp: "4.5s", // Largest Contentful Paint
tti: "5.2s", // Time to Interactive
tbt: "890ms", // Total Blocking Time
cls: "0.15", // Cumulative Layout Shift
lighthouse: "58/100",
seoImpact: "Pénalisé (-15% trafic organique)",
},
minimalistStack: {
// React + Zustand + Tailwind + Native JS + day.js
bundleSize: "220kb gzipped",
fcp: "0.9s",
lcp: "1.4s",
tti: "1.8s",
tbt: "120ms",
cls: "0.02",
lighthouse: "94/100",
seoImpact: "Favorisé (+18% trafic organique)",
},
improvements: {
bundleSize: "-74%",
lcp: "-68%",
tti: "-65%",
tbt: "-86%",
lighthouse: "+62%",
seoTraffic: "+33%",
},
businessImpact: {
conversionRate: "+12% (chargement rapide = plus de ventes)",
bounceRate: "-28%",
seoRevenue: "+45k€/mois (petit e-commerce)",
},
};Stratégies de Réduction des Dépendances
// Audit des dépendances
// 1. Analyser bundle actuel
// npx webpack-bundle-analyzer dist/stats.json
// 2. Trouver des substituts plus légers
const replacements = {
moment: "day.js", // 72kb → 2kb (-97%)
lodash: "native JS", // 24kb → 0kb (-100%)
axios: "native fetch", // 13kb → 0kb (-100%)
uuid: "crypto.randomUUID()", // 4kb → 0kb (-100%)
classnames: "clsx", // 1.5kb → 0.5kb (-66%)
redux: "zustand", // 67kb → 1.2kb (-98%)
};
// 3. Tree-shaking correct
// ❌ Importe tout
import _ from "lodash";
import * as MUI from "@mui/material";
// ✅ Importe uniquement le nécessaire
import debounce from "lodash/debounce";
import { Button, TextField } from "@mui/material";
// 4. Lazy loading
// ❌ Charge tout d'emblée
import Dashboard from "./Dashboard";
import Settings from "./Settings";
import Analytics from "./Analytics";
// ✅ Charge à la demande
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
const Analytics = lazy(() => import("./Analytics"));
// 5. Supprimer le code mort
// npx depcheck (montre deps non utilisées)
// npx unimported (montre imports non utilisés)
// 6. CDN pour libs spécifiques
// Exemple : Chart.js (utilisé sur 1 seule page)
// <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
// Non inclus dans le bundle principalConclusion : Less is More en 2025
Le JavaScript minimaliste n'est pas régresser — c'est évoluer vers des solutions plus efficaces.
Réalité prouvée :
const minimalistBenefits = {
performance: "+60-80% temps de chargement plus rapide",
seo: "+15-30% trafic organique",
maintenance: "-50% temps à déboguer les dépendances",
development: "Vélocité égale ou supérieure (l'IA compense)",
costs: "-30% coûts infrastructure (moins CDN, moins compute)",
};
const actionPlan = {
immediate: [
"Audit dépendances (npx depcheck)",
"Remplacer Lodash par JS natif",
"Considérer Zustand si utilise Redux",
"Lazy load routes lourdes",
],
shortTerm: [
"Refactorer vers Vanilla JS où ça fait sens",
"Évaluer Jotai pour état granulaire",
"Implémenter code splitting agressif",
],
longTerm: ["Web Components pour design system", "Culture zéro-dépendance"],
};Moins de code, plus de résultats.
Si vous voulez en savoir plus sur la performance web moderne, je recommande : WebAssembly + JavaScript Performance.
C'est parti ! 🦅
📚 Vous Voulez Maîtriser le JavaScript Moderne ?
Cet article a montré des techniques minimalistes, mais maîtriser un JavaScript solide est fondamental pour les appliquer correctement.
Matériel d'Étude Complet
J'ai préparé un guide complet JavaScript du basique à l'avancé avec focus sur la performance et les bonnes pratiques :
Options d'investissement :
- €9,90 (paiement unique)
👉 Découvrir le Guide JavaScript
💡 Fondamentaux solides pour construire des applications minimalistes et performantes

