Retour au blog

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 principal

Conclusion : 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

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires