Skip to content

Simplify helper wrapper - #2041

Open
mohd-akram wants to merge 1 commit into
handlebars-lang:masterfrom
mohd-akram:simplify-helper-wrapper
Open

Simplify helper wrapper#2041
mohd-akram wants to merge 1 commit into
handlebars-lang:masterfrom
mohd-akram:simplify-helper-wrapper

Conversation

@mohd-akram

Copy link
Copy Markdown
Contributor

This improves performance slightly and gives the wrapper function a more descriptive name to help with debugging/profiling.

@mohd-akram mohd-akram mentioned this pull request Sep 4, 2024
@jaylinski

Copy link
Copy Markdown
Member

Can you post the setup you are using for measuring the performance improvements?

@mohd-akram

Copy link
Copy Markdown
Contributor Author

@jaylinski Posted the setup + the numbers in #1991. For this change, the difference is small, but we drop an extra function call.

@nknapp

nknapp commented Sep 6, 2024

Copy link
Copy Markdown
Collaborator

I agree that the "wrapHelper" function is a bit redundant. I guess my goal was to also use it in different use-cases.

However, I think, "passLookupPropertyOption" (or "wrapWithLookupPropertyOption") is a more descriptive name than just "wrapHelper". So I would keep that name.

Another thing is that the runtime.js file is already too large for my taste and I would try to extract code into different files where possible and let the bundler put them back together.

In this case, it might be good to extract the whole "addHelpers" or even the four lines where it is called, into a function.

Tbh, I never expected function calls to have a lot of impact on JavaScript performance. Since V8 is an optimizing JIT compiler, I always thought it would inline them when necessary.

@mohd-akram
mohd-akram force-pushed the simplify-helper-wrapper branch from b0e52c3 to 668d2fa Compare September 6, 2024 14:01
@mohd-akram

Copy link
Copy Markdown
Contributor Author

@nknapp Good points, updated the PR. Regarding the performance, I agree about the inlining, and I didn't expect to find any difference, but there is something like 5%. Either way, it's hard to tell what exactly the JIT will do so it doesn't hurt to simplify in this case.

@nknapp

nknapp commented Sep 6, 2024

Copy link
Copy Markdown
Collaborator

Just to get a feeling on how much improvement we have here, I modified you benchmark a little, to only include the "partial" part and to run it multiple times, gathering 10th and 90th percentile of the run time.

const Handlebars = require('./dist/handlebars');

const count = 100000;

// Handlebars
Handlebars.registerPartial('partial', Handlebars.compile('1234'));
const template = Handlebars.compile('{{> partial}}');

let times = []

for (let r = 0; r < 100; r++) {
  // Warm up
  for (let i = 0; i < count; i++) {
    template();
  }

  const start = performance.now()
  for (let i = 0; i < count; i++) {
    template();
  }
  const time = performance.now() - start
  times.push(time)
}

times.sort((a,b) => a - b)

console.log("10th percentile", times[10])
console.log("90th percentile", times[90])

For the 4.x version I got the following numbers:

➜  handlebars.js git:(e914d60) ✗ node ./performance.js 
10th percentile 55.09403999999995
90th percentile 56.883372000000236
➜  handlebars.js git:(e914d60) ✗ node ./performance.js
10th percentile 55.61303500000008
90th percentile 58.357834999999795
➜  handlebars.js git:(e914d60) ✗ node ./performance.js
10th percentile 55.286535000000185
90th percentile 57.09553800000003

For the current version of this PR:

➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js                                                           
10th percentile 52.90112299999964
90th percentile 54.70712900000035
➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js
10th percentile 52.66511000000014
90th percentile 54.73322900000039
➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js
10th percentile 54.04121799999939
90th percentile 55.698201000000154

I see that the difference between 10th and 90th percentile can be 4 to 6 percent.
The PR makes thinks about 6 percent faster, which is just outside of the significance threshold.
That is, for templates that just consist of a partial call and don't do anything else.

There is always this problem with micro-benchmarks, that they have a high variability. This is because they are not deterministic. The computer is doing different things in the background, which might affect the numbers.
The Node.js runtime may or may not apply different optimizations between different runs.
It is difficult to determine why exactly the numbers vary.

So my take on this matter is this:

I don't think removing function calls does much to increase performance in JavaScript, so I would not remove a function just for performance benefits.
If the function makes the code more difficult to understand, that's a different matter.

But usually, the opposite is true. I sometimes extract functions, even if they are only called from a single location

  • in order to give it a descriptive name, and
  • in order to have shorter functions.

Sorry for this wall of text. I got pulled into the rabbit hole somehow.

@nknapp
nknapp self-requested a review September 6, 2024 14:49
@nknapp

nknapp commented Sep 6, 2024

Copy link
Copy Markdown
Collaborator

Oh, I didn't see your comment until now.

@jaylinski I think this can be merged. But ultimately, it is your choice.

@mohd-akram

Copy link
Copy Markdown
Contributor Author

I very much agree with your take, and thanks for verifying the numbers, optimization is definitely a rabbit hole :D

@jaylinski

jaylinski commented Mar 25, 2026

Copy link
Copy Markdown
Member

We now have performance benchmarks in master thanks to @kibertoad. I think we should target this on master and let the performance suite run to see the results.

@mohd-akram

Copy link
Copy Markdown
Contributor Author

Will re-target to master once #2138 is merged.

@mohd-akram
mohd-akram force-pushed the simplify-helper-wrapper branch from 668d2fa to 3898875 Compare April 29, 2026 13:59
@mohd-akram
mohd-akram changed the base branch from 4.x to master April 29, 2026 13:59
This improves performance slightly and gives the wrapper function a more
descriptive name to help with debugging/profiling.
@mohd-akram
mohd-akram force-pushed the simplify-helper-wrapper branch from 3898875 to 445fb70 Compare April 29, 2026 14:11
@mohd-akram

Copy link
Copy Markdown
Contributor Author

Benchmark results:

Benchmark Comparison: master vs simplify-helper-wrapper

Baseline: /Volumes/MyPassport/Code/handlebars.js/tests/bench/results/bench-2026-04-29T14-14-22-master.md
Current: /Volumes/MyPassport/Code/handlebars.js/tests/bench/results/bench-2026-04-29T14-20-26-simplify-helper-wrapper.md

Legend: ! = >2% change, !! = >5% change

COMPILATION (Handlebars.compile + first render)

Benchmark master ops/sec simplify-helper-wrapper ops/sec ops/sec p75 latency
compile: arguments (positional + hash) 21.71K 24.97K +15.0% !! -13.1% !!
compile: complex (if/each/helpers) 5.62K 5.73K +2.0% +1.5%
compile: deeply nested context (4 levels) 6.82K 5.50K -19.4% !! +102.0% !!
compile: depth-1 (../) 25.96K 29.58K +13.9% !! -13.1% !!
compile: dot paths 18.73K 20.59K +9.9% !! -5.5% !!
compile: each (large array, 100 items) 12.71K 10.47K -17.6% !! +96.0% !!
compile: each (large array, 1000 items) 21.25K 19.39K -8.8% !! +59.6% !!
compile: each (small array, 4 items) 23.68K 20.08K -15.2% !! +105.1% !!
compile: each with @index/@key 14.29K 15.57K +9.0% !! -21.5% !!
compile: if/else conditional 12.87K 15.23K +18.3% !! -39.0% !!
compile: many partials (10 partials) 9.05K 8.51K -6.0% !! +11.6% !!
compile: mustache-style section (array) 20.64K 21.66K +4.9% ! -2.9% !
compile: mustache-style section (object) 15.48K 15.32K -1.0% -2.5% !
compile: nested depth (../../) 14.11K 14.72K +4.3% ! -14.7% !!
compile: page template (mixed features) 3.36K 3.34K -0.6% +2.2% !
compile: partial (each + partial) 23.38K 24.11K +3.1% ! -15.5% !!
compile: recursive partials 19.07K 16.67K -12.6% !! +51.8% !!
compile: simple variables 27.36K 29.62K +8.3% !! -3.8% !
compile: static string (no expressions) 245.71K 249.63K +1.6% +1.0%
compile: subexpressions 34.18K 33.86K -0.9% -0.4%
compile: with helper 17.80K 20.49K +15.1% !! -12.8% !!

COMPILE OPTIONS COMPARISON

Benchmark master ops/sec simplify-helper-wrapper ops/sec ops/sec p75 latency
exec: compat=true 646.64K 672.69K +4.0% ! -2.6% !
exec: data=false 867.19K 886.54K +2.2% ! -3.4% !
exec: default options 836.30K 869.80K +4.0% ! -3.4% !
exec: knownHelpers 837.48K 902.95K +7.8% !! -4.3% !
exec: noEscape=true 879.50K 909.86K +3.5% ! -4.4% !
exec: strict + assumeObjects 769.12K 802.29K +4.3% ! -3.1% !

END-TO-END (compile + render)

Benchmark master ops/sec simplify-helper-wrapper ops/sec ops/sec p75 latency
e2e: arguments (positional + hash) 24.48K 24.90K +1.7% -0.4%
e2e: complex (if/each/helpers) 5.90K 7.26K +23.1% !! -27.6% !!
e2e: deeply nested context (4 levels) 7.27K 7.79K +7.2% !! -4.9% !
e2e: depth-1 (../) 30.02K 29.85K -0.6% +0.1%
e2e: dot paths 19.25K 20.15K +4.7% ! -2.9% !
e2e: each (large array, 100 items) 11.25K 12.34K +9.7% !! -6.5% !!
e2e: each (large array, 1000 items) 10.23K 10.98K +7.3% !! -4.0% !
e2e: each (small array, 4 items) 26.06K 26.86K +3.1% ! -3.1% !
e2e: each with @index/@key 18.90K 18.93K +0.2% -0.2%
e2e: if/else conditional 17.29K 17.14K -0.9% +0.7%
e2e: many partials (10 partials) 9.24K 9.97K +7.9% !! -5.1% !!
e2e: mustache-style section (array) 23.05K 23.07K +0.1% -0.2%
e2e: mustache-style section (object) 18.15K 18.34K +1.0% -0.6%
e2e: nested depth (../../) 15.87K 15.63K -1.5% +1.1%
e2e: page template (mixed features) 3.29K 3.70K +12.5% !! -12.2% !!
e2e: partial (each + partial) 28.89K 28.55K -1.2% +1.2%
e2e: recursive partials 18.43K 20.79K +12.8% !! -7.4% !!
e2e: simple variables 29.54K 30.66K +3.8% ! -3.7% !
e2e: static string (no expressions) 239.56K 255.47K +6.6% !! -2.9% !
e2e: subexpressions 39.66K 39.73K +0.2% 0.0%
e2e: with helper 17.45K 20.30K +16.3% !! -21.9% !!

EXECUTION (template rendering)

Benchmark master ops/sec simplify-helper-wrapper ops/sec ops/sec p75 latency
exec: arguments (positional + hash) 2.14M 2.64M +23.4% !! -25.0% !!
exec: complex (if/each/helpers) 725.42K 868.56K +19.7% !! -17.5% !!
exec: deeply nested context (4 levels) 194.89K 221.15K +13.5% !! -8.5% !!
exec: depth-1 (../) 1.09M 1.22M +11.9% !! -9.1% !!
exec: dot paths 2.02M 2.12M +5.0% ! -8.2% !!
exec: each (large array, 100 items) 61.42K 72.23K +17.6% !! -43.5% !!
exec: each (large array, 1000 items) 16.92K 18.37K +8.6% !! -18.4% !!
exec: each (small array, 4 items) 1.58M 1.64M +3.8% ! -6.6% !!
exec: each with @index/@key 1.22M 1.27M +4.1% ! 0.0%
exec: if/else conditional 2.09M 2.39M +14.4% !! -9.2% !!
exec: many partials (10 partials) 503.01K 532.04K +5.8% !! -2.1% !
exec: mustache-style section (array) 1.51M 1.61M +6.6% !! 0.0%
exec: mustache-style section (object) 2.04M 2.11M +3.4% ! -8.2% !!
exec: nested depth (../../) 458.56K 498.85K +8.8% !! -3.8% !
exec: page template (mixed features) 82.28K 98.38K +19.6% !! -10.9% !!
exec: partial (each + partial) 1.06M 1.11M +4.7% ! -0.1%
exec: recursive partials 874.94K 928.59K +6.1% !! -4.4% !
exec: simple variables 2.46M 2.56M +4.1% ! -10.1% !!
exec: static string (no expressions) 3.26M 3.34M +2.5% ! 0.0%
exec: subexpressions 2.22M 2.55M +14.9% !! -9.2% !!
exec: with helper 2.05M 2.29M +11.7% !! -8.4% !!

PRECOMPILATION (Handlebars.precompile)

Benchmark master ops/sec simplify-helper-wrapper ops/sec ops/sec p75 latency
precompile: arguments (positional + hash) 22.60K 24.80K +9.7% !! -4.5% !
precompile: complex (if/each/helpers) 6.48K 7.66K +18.2% !! -20.5% !!
precompile: deeply nested context (4 levels) 7.22K 8.03K +11.2% !! -13.6% !!
precompile: depth-1 (../) 26.18K 30.40K +16.1% !! -9.6% !!
precompile: dot paths 17.58K 20.60K +17.2% !! -21.8% !!
precompile: each (large array, 100 items) 13.46K 15.64K +16.2% !! -19.7% !!
precompile: each (large array, 1000 items) 23.47K 26.53K +13.0% !! -7.7% !!
precompile: each (small array, 4 items) 24.02K 27.84K +15.9% !! -9.3% !!
precompile: each with @index/@key 16.17K 20.05K +24.0% !! -30.7% !!
precompile: if/else conditional 15.36K 17.88K +16.4% !! -15.5% !!
precompile: many partials (10 partials) 9.43K 10.59K +12.3% !! -12.8% !!
precompile: mustache-style section (array) 21.14K 24.13K +14.1% !! -8.2% !!
precompile: mustache-style section (object) 15.71K 19.04K +21.2% !! -26.1% !!
precompile: nested depth (../../) 14.55K 16.54K +13.7% !! -9.5% !!
precompile: page template (mixed features) 3.45K 3.95K +14.5% !! -15.5% !!
precompile: partial (each + partial) 26.60K 29.89K +12.4% !! -6.4% !!
precompile: recursive partials 18.83K 21.69K +15.2% !! -13.9% !!
precompile: simple variables 25.02K 31.76K +26.9% !! -42.8% !!
precompile: static string (no expressions) 184.46K 210.52K +14.1% !! -7.4% !!
precompile: subexpressions 34.98K 41.65K +19.1% !! -17.5% !!
precompile: with helper 18.97K 20.05K +5.7% !! -2.9% !

@jaylinski jaylinski removed their assignment May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants