Méthode vs Fonction comme propriété dans les classes JS/TS : Expérimentation de Profilage Node.js
Est-ce que les “arrow function” comme propriétés de classe coûtent plus cher ? La théorie nous dit “oui”, vérifions cela en pratique !
Méthode vs Fonction comme propriété dans les classes JS/TS : Expérimentation de Profilage Node.js

Introduction
La dernière fois, j’effectuais une relecture d’un article écrit par mon collègue Christophe Vaudry et je suis tombé sur la citation suivante :
Avec une arrow function, vous assumez un coût en temps et en mémoire lié à la création d’une nouvelle fonction par instance.
Je me suis donc demandé (et j’ai aussi demandé à mon collègue) :
Vraiment ? Avons-nous des métriques pour soutenir cela ?
Voici un bref aperçu de notre discussion :

Illustration de notre discussion avec mon collègue Christophe V. — Cette petite bande dessinée est générée par IA, j’en assume pleinement ces défauts et son côté “cringe”
*- Moi : Nous devrions au moins ajouter un benchmark rapide pour illustrer et soutenir cette déclaration, n’est-ce pas ?
- Christophe V. : Ouais, bonne idée. Mais cela dépasse le champ de l’article 😕
- Christophe V. : Et je ne me sens pas à l’aise avec JS/TS pour le profiler sans faire des recherches sur comment le faire. Cela prendra du temps et retardera la publication de l’article 😕
- Moi : Eh bien, je connais assez bien JS/TS et je sais comment le profiler *😐**
Ainsi, il a donc décidé que je devrais écrire un article sur le sujet. Et nous y voilà 🙂
Méthode vs Fonction comme propriété dans les classes JS/TS — Le Code
Afin de profiler la différence entre les méthodes et les fonctions utilisées comme propriétés dans les classes JavaScript / TypeScript, je vais commencer par du code.
Nous aurons deux fichiers. Deux implémentations qui font exactement la même chose. L’une avec des méthodes dans les classes et l’autre avec des fonctions comme propriétés dans les classes.
object-method-bench.js:
[embed]
function-property-bench.js:
[embed]
Que font ces fichiers ?
- Déclarer 3 classes :
LineProcessor,ResultsComputeretUnderTest. - Créer une nouvelle instance de
UnderTestet appeler immédiatement sa méthodemain.
Oui, et alors ?
Et bien voici l’idée globale : lire un fichier d’entrée où chaque ligne contient une opération mathématique simple entre 2 opérandes et additionner les résultats de ces opérations pour enfin imprimer le total dans la console.
Afin de profiler quelque chose et d’observer s’il existe une différence significative, nous allons devoir effectuer des tests sur un large ensemble de données. Nous devrions avoir la création de nombreuses instances de classe et diverses appels de méthodes / fonctions comme propriétés.
C’est ce qui est fait ici. Ces implémentations ne prétendent pas être les plus efficaces, mais sont là pour illustrer ce que nous essayons d’observer et de prouver :
Existe-t-il vraiment une différence de performance ou d’empreinte CPU / Mémoire entre les méthodes et les fonctions utilisées comme propriétés dans les classes JavaScript / TypeScript ?
JavaScript / TypeScript — Comment profiler ?
Nous pourrions faire une recherche rapide avec notre outil préféré : « Comment profiler une application JavaScript/TypeScript ? »
Puisque nous parlons de l’écosystème JavaScript, cela listera à coup sûr de nombreuses bibliothèques et outils sur NPM, et sans doute certains qui prétendent être « rapides » parce qu’ils sont écrits en Rust 🙃
Oui, MAIS : je vais peut-être paraitre « vieux jeu », mais nous allons d’abord consulter la documentation officielle.
Il y a une section “*Profiling Node.js applications*“ dans la section de démarrage.
TL;DR; -> Node.js a un profileur intégré. Pas besoin d’installer des dépendances pour commencer 🙂
ET : Si nous consultons les spécifications du langage JavaScript, nous pouvons trouver qu’il existe une API “Performance” 😯. Cela signifie que nous pourrions même profiler du code JavaScript / TypeScript en étant complètement indépendant du runtime ?!?
Tout cela semble être un bon point de départ 🙂
JavaScript / TypeScript — Le Profilage
Si nous résumons :
- Nous avons la question à laquelle nous essayons de répondre
- Nous avons un scénario de benchmark
- Nous avons les éléments de comparaison
- Nous avons les outils pour observer ce qui se passe pendant l’exécution
C’est parti !
Voici la configuration que j’utilise pour effectuer l’expérience :
- Un ordinateur portable professionnel (un MacBook Pro 16 pouces 2024)
- Un terminal (Warp)
- Un IDE (VS Code)
- NVM avec Node.js lts/krypton (24) utilisé pour le projet
J’ai généré un fichier d’entrée contenant une centaine de lignes (avec un script JavaScript ou avec un prompt LLM basique).
Commençons par suivre la documentation de Node.js :
Première exécution de fichier :
$> NODE_ENV=production node --prof object-method-bench.js
Final Result: 6364359
Deuxième exécution de fichier :
$> NODE_ENV=production node --prof function-property-bench.js
Final Result: 6364359
Une fois que cela est fait, nous devrions voir deux nouveaux fichiers créés dans le répertoire courant (nommés isolate-0xnnn-v8.log)
Jetons un œil à leur contenu 🤓 :
v8-version,13,6,233,17,-node.37,0
v8-platform,macos,macos
shared-library,/Users/tverhoken/.nvm/versions/node/v24.13.0/bin/node,0x100828000,0x102ac8268,8536064
shared-library,/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation,0x1892646f0,0x18945dcd4,148062208
shared-library,/usr/lib/libobjc.A.dylib,0x188dc1400,0x188e01c4c,148062208
shared-library,/System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal,0x18d4faf20,0x18d531bfc,148062208
shared-library,/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation,0x18aad14c0,0x18b6b9414,148062208
shared-library,/usr/lib/liboah.dylib,0x1990137d8,0x199019034,148062208
shared-library,/usr/lib/libfakelink.dylib,0x199048638,0x199049b48,148062208
shared-library,/usr/lib/libicucore.A.dylib,0x18cf228e8,0x18d1816a8,148062208
shared-library,/usr/lib/libSystem.B.dylib,0x1990471a0,0x1990477bc,148062208
shared-library,/System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking,0x19904a5d0,0x19904a86c,148062208
shared-library,/usr/lib/swift/libswiftCore.dylib,0x19c8b22a0,0x19cd4dfac,148062208
shared-library,/usr/lib/libc++abi.dylib,0x1891c5308,0x1891da7b4,148062208
shared-library,/usr/lib/libRosetta.dylib,0x28b9297d8,0x28b92f034,148062208
shared-library,/usr/lib/libc++.1.dylib,0x1891345f8,0x189198168,148062208
shared-library,/usr/lib/swift/libswiftObjectiveC.dylib,0x1a640df60,0x1a640f684,148062208
shared-library,/usr/lib/libswiftPrespecialized.dylib,0x28ce9ee38,0x28ce9ee38,148062208
shared-library,/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration,0x18a73ebc0,0x18a7c1aac,148062208
shared-library,/usr/lib/libz.1.dylib,0x198f90640,0x198f9bf28,148062208
shared-library,/System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout,0x193aa2ef8,0x193ad501c,148062208
shared-library,/usr/lib/libcompression.dylib,0x19926e7c0,0x1992c5abc,148062208
shared-library,/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork,0x18f579e78,0x18f7da874,148062208
shared-library,/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration,0x192ed2a90,0x192ed9e68,148062208
shared-library,/usr/lib/libarchive.2.dylib,0x19909b9a8,0x19917dca0,148062208
shared-library,/usr/lib/libDiagnosticMessagesClient.dylib,0x192dd76e8,0x192dd8364,148062208
shared-library,/usr/lib/libxml2.2.dylib,0x193aeba08,0x193bb1998,148062208
shared-library,/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices,0x1a16da950,0x1a16da950,148062208
shared-library,/usr/lib/liblangid.dylib,0x196c435e8,0x196c4411c,148062208
...
😳 Soyons honnêtes, cela n’a pas du tout l’air ni convivial ni amusant à lire.
Ces fichiers contiennent des milliers de lignes et ces lignes décrivent les instructions “tick” qui se sont produites pendant l’exécution du programme Node.js.
La documentation décrit cela et nous dit d’utiliser le processeur de “tick” fourni avec Node.js pour donner un sens à ces fichiers 🙂
$> node --prof-process isolate-0x111-v8.log > object-method-bench-processed.txt
$> node --prof-process isolate-0x222-v8.log > function-property-bench-processed.txt
Jetons un œil à leur contenu 🤓 :
Statistical profiling result from isolate-0x90280c000-19794-v8.log, (48 ticks, 2 unaccounted, 0 excluded).
[Shared libraries]:
ticks total nonlib name
7 14.6% /usr/lib/system/libsystem_c.dylib
3 6.3% /usr/lib/libc++.1.dylib
1 2.1% /usr/lib/libobjc.A.dylib
[JavaScript]:
ticks total nonlib name
[C++]:
ticks total nonlib name
20 41.7% 54.1% t std::__1::__shared_ptr_emplace<node::SocketAddress, std::__1::allocator<node::SocketAddress>>::~__shared_ptr_emplace()
3 6.3% 8.1% T _pthread_key_create
3 6.3% 8.1% T ___pthread_init
2 4.2% 5.4% t std::__1::ostreambuf_iterator<char, std::__1::char_traits<char>> std::__1::__pad_and_output[abi:nn180100]<char, std::__1::char_traits<char>>(std::__1::ostreambuf_iterator<char, std::__1::char_traits<char>>, char const*, char const*, char const*, std::__1::ios_base&, char)
2 4.2% 5.4% T _pthread_key_init_np
2 4.2% 5.4% T _chmod
1 2.1% 2.7% t std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:nn180100]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned long)
1 2.1% 2.7% t __os_once_gate_wait
1 2.1% 2.7% T __kernelrpc_mach_port_get_refs
[Summary]:
ticks total nonlib name
0 0.0% 0.0% JavaScript
35 72.9% 94.6% C++
1 2.1% 2.7% GC
11 22.9% Shared libraries
2 4.2% Unaccounted
[C++ entry points]:
ticks cpp total name
19 63.3% 39.6% t std::__1::__shared_ptr_emplace<node::SocketAddress, std::__1::allocator<node::SocketAddress>>::~__shared_ptr_emplace()
3 10.0% 6.3% T _pthread_key_create
3 10.0% 6.3% T ___pthread_init
2 6.7% 4.2% t std::__1::ostreambuf_iterator<char, std::__1::char_traits<char>> std::__1::__pad_and_output[abi:nn180100]<char, std::__1::char_traits<char>>(std::__1::ostreambuf_iterator<char, std::__1::char_traits<char>>, char const*, char const*, char const*, std::__1::ios_base&, char)
2 6.7% 4.2% T _pthread_key_init_np
1 3.3% 2.1% t std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:nn180100]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned long)
...
Cela semble un poil plus facile à lire, mais soyons honnêtes : cela ne semble toujours pas très convivial ni amusant à lire !
Ces fichiers affichent des informations par section et par language. Il existe une section “Summary” qui nous donne la distribution de ce que le programme a utilisé.
Maintenant que nous avons ces fichiers, nous pouvons commencer à les analyser et essayer de trouver des différences entre les deux implémentations. Peut-être pouvons-nous utiliser certains outils pour cela ? Essayons un LLM, car cela semble être le nouveau remède miracle pour tout en ce moment 🫠
Voici le prompt que j’ai utilisé pour demander au LLM (Github Copilot avec le modèle Claude Sonnet 4.6) d’analyser ces fichiers et de me donner des informations sur les différences entre les deux implémentations :
Can you analyze these Node.js profiling files and point out the key differences?
Et voici les résultats :
## What's being compared
| Bench | Pattern |
|-------|---------|
| `object-method-bench.js` | Prototype methods + `.bind(this)` |
| `function-property-bench.js` | Arrow function class fields (per-instance) |
## Key profiling differences
### 1. Garbage Collection
| | GC ticks |
|-------|---------|
| Object method | **1 tick (2.1%)** |
| Function property | **0 ticks (0.0%)** |
Counterintuitive: arrow function class fields create one function object per instance, so you'd expect more GC pressure. The single GC tick in the method version may be linked to the .bind(this) call in promiseWorkerBuilder - bind() allocates a new bound function object on every processLine() call.
### 2. Unaccounted ticks
| | Unaccounted |
|-------|---------|
| Object method | 2 ticks (4.2%) |
| Function property | **5 ticks (10.2%)** |
Higher unaccounted ticks often indicate code that V8 has heavily optimized or inlined, making it invisible to the statistical profiler. This slightly favors the function-property approach at runtime.
### 3. Top C++ hotspot share
Both profiles are dominated by `std::__1::__shared_ptr_emplace<node::SocketAddress>::~__shared_ptr_emplace()` (Node.js socket/stream initialization), but it takes a **larger** slice in the method bench:
| | Share of total ticks |
|-------|---------|
| Object method | **39.6%** |
| Function property | **28.6%** |
### 4. System call differences
- Object method bench hits `_pthread_key_create` + `___pthread_init`
- Function property bench hits `_mig_get_reply_port` + `__pthread_mutex_global_init`
These are different threading primitives being initialized, likely due to minor execution-path differences caused by timing, not the pattern itself.
### 5. The JavaScript tick problem
Both profiles show **0% JavaScript ticks**. This is the most important observation: V8 JIT-compiled the hot paths in both cases so thoroughly that the statistical sampler never caught them executing in interpreted JS. This makes it impossible to identify hotspots within your own code from these profiles alone.
## Bottom line
These profiles are **dominated by Node.js startup/initialization** (streams, ESM loader, TTY, etc.) with only **48–49 total ticks** - far too small a sample for statistically meaningful conclusions. The only user-code frame visible in both is `outputResult` appearing once via `Writable.write`.
To get actionable profiling data you'd need to:
- Increase the input size to extend runtime.
L’information importante ici se trouve dans les dernières sections :
Both profiles show 0% JavaScript ticks.
The profiles are dominated by Node.js startup / initialization…
C’est complètement normal et voici pourquoi : mon fichier d’entrée ne contient que 100 lignes. Ce n’est pas suffisant pour avoir un temps d’exécution significatif pour pouvoir échantillonner l’exécution du code JavaScript.
Faisons en sorte de pouvoir générer du “Big Data” et réessayons. Et ici encore, je vais jouer avec un LLM.
Voici le prompt que j’ai utilisé pour demander au LLM (Github Copilot avec le modèle Claude Sonnet 4.6) de générer un fichier d’entrée avec 1 million de lignes :
Can you generate a file named `input.txt` where each line will be a simple mathematical operation ?
I want these mathematical operations to be :
- Addition, represented by the symbol "+"
- Subtraction, represented by the symbol "-"
- Multiplication, represented by the symbol "*"
- Division, represented by the symbol "%"
Each operation should be done with 2 operands and with integer only.
I want at least 1 million lines for that input file.
Maintenant que nous avons notre fichier d’entrée « volumineux », exécutons les scripts à nouveau et générons de nouveaux fichiers de profilage pour une nouvelle comparaison.
Voici les résultats :
## Summary comparison
| Metric | Object method | Function property |
|--------|---------------|-----------------|
| Total ticks | 271 | 285 |
| JS ticks | 32 (11.8%) | 30 (10.5%) |
| GC ticks | **71 (26.2%)** | **79 (27.7%)** |
| Unaccounted | 5 (1.8%) | 8 (2.8%) |
## Key differences
### 1. GC pressure - the most significant finding
The function-property approach **has more garbage collection** (+8 ticks, +1.5 percentage points).
This is the expected theoretical cost confirmed empirically: arrow function class fields create a **new function object per instance** (e.g. each `new LineProcessor()` allocates fresh `processLine`, `promiseWorkerBuilder` functions on the heap). With one `LineProcessor` created per line, this scales linearly with input size. Prototype methods are shared across all instances.
### 2. `promiseWorker` visibility
| Profile | `promiseWorker` appearance |
|---------|---------------------------|
| Object method | +`promiseWorker` - 1 tick, **baseline tier** (partially optimized) |
| Function property | Absent entirely |
The `+` prefix means V8's Maglev/Sparkplug baseline compiled it but didn't fully JIT it. In the function-property version, the arrow function equivalent is completely invisible - either **inlined into the caller** or handled at a different optimization boundary. This is reflected in the higher unaccounted ticks of the function-property profile.
### 3. RegExp overhead
| Profile | RegExp ticks |
|---------|-------------|
| Object method | 12 ticks (4.4%) |
| Function property | 10 ticks (3.5%) |
Both profiles show the `RegExp: [+\-*%]` as the dominant regex cost (called inside `processLine`). The object-method version spends proportionally slightly more time in the regex engine.
### 4. Both achieve equivalent JIT optimization for hot paths
Both `processLine` and `accumulateResults` are marked `*` (fully TurboFan-optimized) in **both** profiles, meaning V8 reached the same optimization level for the actual business logic regardless of the pattern used.
`processFileContent`, `processLines`, `clearContent` remain `~` (unoptimized/lazy) in both - they run too infrequently to trigger JIT compilation.
## Bottom line
| | Object method | Function property |
|---|---------------|-----------------|
| GC | Lighter ✓ | Heavier (per-instance fns) |
| Inlining | Less aggressive | More aggressive (unaccounted ↑) |
| Hot path JIT | Identical (`*`) | Identical (`*`) |
| Net verdict | Marginally lighter on memory | Marginally better code erasure |
The difference is real but small. The GC overhead from per-instance arrow functions is the only measurable cost of the function-property pattern at this scale. If `LineProcessor` were longer-lived or pooled, that cost would disappear entirely.
Désormais, nous avons plus d’informations.
Pour faire court : nous pouvons observer que l’approche « function property » a plus d’activité “garbage collection”. Cela semble logique car en théorie, les “arrow function” en tant que propriété de classe devraient créer un nouvel objet fonction par instance, tandis que les méthodes de classes sont attachées au prototype et sont partagées entre toutes les instances.
Conclusion ?
Bien, il semble que nous pouvons prouver en pratique ce que la théorie nous a annoncé.
Mais est-ce bien satisfaisant jusqu’ici ? Le profilage effectué jusqu’à présent ne nous montre que certaines activités du moteur JavaScript. Qu’en est-il de l’utilisation classique du CPU et de la RAM ?
Essayons de récolter de bonnes vieilles métriques !
Une fois de plus, nous pouvons utiliser la documentation officielle pour nous apprendre comment faire. Il y a une section complète « diagnostics », alors commençons par là.
La bonne nouvelle est que si vous avez un navigateur sur base Chromium disponible, vous pouvez l’utiliser pour enregistrer l’utilisation du CPU et de la mémoire de votre application Node.js.
C’est parti !
En se référant à la documentation, nous exécuterons nos scripts avec l’option --inspect-brk afin de mettre en pause le script jusqu'à ce que le débogueur soit attaché.
$> NODE_ENV=production node --inspect-brk object-method-bench.js
$> NODE_ENV=production node --inspect-brk function-property-bench.js
Ensuite, nous ouvrirons la page chrome://inspect dans notre navigateur et commencerons l'enregistrement de l'utilisation du CPU et de la mémoire.
Voici les instantanés de l’enregistrement d’utilisation du CPU pour notre exemple :

Chromium CPU profiling results — object-method-bench.js

Chromium CPU profling results — function-property-bench.js
Pas de grande différence ici. Mais nous avons des graphiques de type “flame graph” et nous pouvons fouiller dans les fonctions et leurs appels afin de voir s’il existe une différence dans la façon dont le temps CPU est distribué entre les deux implémentations.
Voici les instantanés de l’enregistrement d’utilisation de la mémoire pour notre exemple :

Chromium memory profiling results — object-method-bench.js

Chromium memory profiling results — function-property-bench.js
Ici, nous pouvons voir une différence plus importante. L’implémentation avec les arrow function comme propriétés de la classe semble avoir une utilisation de la mémoire plus élevée que l’implémentation avec les méthodes de classe. Ceci est cohérent avec ce que nous avons observé dans les fichiers de profilage où nous avions plus d’activité de “garbage collection” pour l’implémentation avec fonction comme propriété de classe.
Réflexions finales
Finalement, nous avons effectué une expérience et observé qu’il existe effectivement une différence entre l’utilisation de méthodes et de fonctions comme propriétés dans les classes JavaScript / TypeScript.
C’est tout ?!? Tout ce travail pour ça ?!?
Eh bien, oui. Et non.
Nous avons non seulement vérifié la théorie avec une petite expérience pratique, mais nous avons également appris à profiler une application Node.js et à analyser les résultats. Le tout sans installer aucune dépendance, en utilisant simplement les outils intégrés fournis par Node.js et le navigateur… Et un LLM… En effet, il se trouve que ce sont de bons outils pour aider à analyser rapidement des données qui ne sont pas évidentes à analyser pour un être humain (par taille ou par complexité). N’oublions pas qu’ils nous remontent des indicateurs et qu’il s’agit d’informations qui doivent être analysées et interprétées par un humain.
Je voudrais également souligner que l’exemple utilisé ici est loin d’être une implémentation « de pointe ». Certaines choses sont délibérément laissées non optimisées afin d’avoir plus de création d’instances de classe et plus d’appels de méthodes / fonctions comme propriétés qu’une implémentation plus optimisée ne le ferait.
Je vous invite à jouer avec les implémentations et à utiliser des “arrow function” pour certaines parties qui sont extraites en tant que méthodes / fonctions comme propriétés. Vous observerez comment le moteur Node.js optimise le code ou comment tous ces fonctions anonymes sont une pure joie à tracer et déboguer dans les outils de profilage 🙃
Resources
- Profiling Node.js applications : https://nodejs.org/en/learn/getting-started/profiling
- Diagnostics Node.js applications : https://nodejs.org/learn/diagnostics/user-journey
Remerciements
Je tiens à remercier mon collègue Christophe Vaudry pour ses relectures attentives ainsi que de m’avoir “provoqué” un petit peu à écrire cet article 😉
[embed]Until next time
메타데이터
- post_id
- 54f81ee331aa
- slug
- méthode-vs-fonction-comme-propriété-dans-les-classes-js-ts-expérimentation-de-profilage-node-js-54f81ee331aa
- url
- https://medium.com/norsys-octogone/m%C3%A9thode-vs-fonction-comme-propri%C3%A9t%C3%A9-dans-les-classes-js-ts-exp%C3%A9rimentation-de-profilage-node-js-54f81ee331aa
- canonical_url
- https://medium.com/norsys-octogone/m%C3%A9thode-vs-fonction-comme-propri%C3%A9t%C3%A9-dans-les-classes-js-ts-exp%C3%A9rimentation-de-profilage-node-js-54f81ee331aa
- author_url
- https://medium.com/@tverhoken
- status
- ok
- fetched_at
- 2026-06-26 12:24:55