Improving the performance of JavaScript string concatenation

Recently, I have been working on implementing a basic logging system for a JavaScript library. Here is an example of what I've come up with:

class Logger {
  static level: 'error' | 'info' = 'error';

  static error(message: string) {
    if (this.level === 'error') {
      console.error(message);
    }
  }

  static info(message: string) {
    if (this.level === 'info') {
      console.info(message);
    }
  }
}

function app() {
  Logger.level = 'info';

  Logger.error('An error occurred at ' + new Date());
  Logger.info('Application started at ' + new Date());
}

While this approach works, I've noticed that some of my colleagues who are working in Java and C# are using lambdas or callbacks to pass messages to their loggers. Their implementation looks like this:

class Logger {
  static level: 'error' | 'info' = 'error';

  static error(message: () => string) {
    if (this.level === 'error') {
      console.error(message());
    }
  }

  static info(message: () => string) {
    if (this.level === 'info') {
      console.info(message());
    }
  }
}

function app() {
  Logger.level = 'info';

  Logger.error(() => 'An error occurred at ' + new Date());
  Logger.info(() => 'Application started at ' + new Date());
}

The argument for the second implementation is that only one log level is active at a time. Therefore, if the `error` level is enabled, the `info` method will not execute the `message()` function, resulting in more optimized code and performance.

Despite this reasoning, after conducting numerous tests involving complex concatenations and comparisons between using callbacks and plain concatenation, I have not observed any noticeable differences in execution times or memory usage. It seems that modern browser compilers may already optimize conditional concatenation efficiently, but I have not found concrete evidence supporting this theory while exploring AST Explorer and Compiler Explorer.

Question : Can anyone confirm whether such optimizations make sense for modern JavaScript? If so, are there any reliable resources or methods to validate these optimizations in practice?

Answer №1

I tested both of your versions and updated the app function with:

  for (let i = 0; i < 200000; i++) {
    Logger.error('Error occurred at ' + new Date());
    Logger.info('App initiated at ' + new Date());
  }

(or using lambdas) I removed the console.log while ensuring to still call message() in the lambda version.

The lambda version executes in about 0.5 seconds, while the non-lambda version takes around 1 second.

It appears that Lambdas do make a difference in performance. However, the overhead of using console.log is higher than combining strings and new Date(). When I reintroduced console.log, both versions performed equally. If your logs are not frequently triggered (e.g., if level is set to info in production but there are many Logger.error calls), utilizing Lambdas can improve speed.
I compared two versions of your code where the main loop only has Logger.info when Logger.level is error (meaning no logging occurs). In this scenario, the lambda version is approximately 15 times faster than the non-lambda version. The primary performance impact comes from calling new Date() due to invocation and memory allocation.

The optimal approach would likely involve placing new Date() inside the error and info functions. String concatenation is highly efficient in V8 because it utilizes "cons strings": when you concatenate "aaaaaaaaa" with "bbbbbbbbb", V8 creates a ConsString object that references both substrings separately. The collapsing into a sequential string occurs only when necessary, such as during printing.


Although the question was marked with the "compilation" tag, here are insights on how V8 could potentially equalize the speed of both versions (with and without lambdas) through inlining, constant folding, dead code elimination, and optimizations/deoptimizations. Here's an overview of how this mechanism might operate.

Inlining. Turbofan typically inlines small functions, and may also inline larger functions if sufficient inlining budget is available. Inside your app function,

Logger.error('Error occurred at ' + new Date());
Logger.info('App initiated at ' + new Date());

is usually inlined. Subsequently, app essentially looks like:

if (this.level === 'error') {
    console.error('Error occurred at ' + new Date());
}
if (this.level === 'info') {
    console.info('App initiated at ' + new Date());
}

Following this, two possibilities arise:

Constant folding. Turbofan might identify that this.level is consistent (has been assigned a value and remains unchanged), resulting in replacing this.level with its value. Consequently, checks become <some value> === 'error' and <some value> === 'info'. These checks are evaluated at compile time, leading to the replacement with true or false. Unreachable IF bodies are then eliminated (termed dead code elimination). Notwithstanding, initialyl,

'Error occurred at ' + new Date()
was part of an argument within the function, resembling:

let message = 'Error occurred at ' + new Date();
if (this.level === 'error') {
    console.log(message);
}

Hence, dead code elimination predominantly eradicates console.log(message), and possibly discards the string concatenation (since the result is unused), leaving out new Date() due to lacking comprehension regarding its side effects.

Note that speculative constant folding entails some risk: any alteration in this.level later prompts V8 to discard the Turbofan-generated code (being incorrect) and trigger recompilation.

If Turbofan cannot speculate on this.value (either due to inconsistency or varied console.error calls across different Logger objects), optimization is still plausible.

Evidently, Turbofan refrains from generating assembly code for JS sections devoid of feedback. Instead, it produces deoptimizations, special instructions enabling reverting to the interpreter. Following inlining, provided each run of the function sets this.level to info, Turbofan might form something akin to:

if (this.level === 'error') {
    Deoptimize();
}
if (this.level === 'info') {
    console.info('App started at ' + new Date());
}

Subsequently,

'Error occurred at ' + new Date()
gets completely excised from the graph, thereby being "free". Any modification to
this.level</code activates the <code>Deoptimize()
instruction, reverting back to bytecode interpretation for executing
console.error('Error occurred at ' + new Date());
. While theoretically viable, achieving this isn't straightforward as the initial computation of
'Error occurred at ' + new Date()
precedes the if condition. Therefore, the graph more closely resembles:

let message = 'Error occurred at ' + new Date();
if (this.level === 'error') {
    Deoptimize(message)
}

(the Deoptimization consistently requires inputs describing extant values within the function).

At this juncture, potential code motion could facilitate shifting

let message = 'Error occurred at ' + new Date();
into the if block, preventing premature computation when unnecessary. Nevertheless, Turbofan’s code motions' efficacy is modest, failing to encapsulate new Date() adequately.


A closing remark: Turbofan often conducts concatenation of fixed strings at compilation stage. While irrelevant to + Date(), benchmarking

"hello " + "world"
mirrors assessing "hello world".

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Generating a random selection of files from a directory and distributing them randomly

In an attempt to create a command for my Discord.js bot that selects a random image from a folder and sends it, I am facing an issue. The array containing the images cannot be set manually due to the constantly changing number of images in the folder. The ...

Solving an object in ui-router state using ui-sref

Dealing with a large JSON object in an Angular controller and wanting to pass it to the controller of a template that will be displayed in a ui-view. I am aware that parameters can be passed to states using ui-sref, but I do not want this object to be visi ...

Issue with creating req.session in Node.js client-sessions library

Encountering troubles with node-client-sessions, I decided to test out a sample application found at https://github.com/fmarier/node-client-sessions-sample. Despite the simplicity of this app, I am consistently faced with an error: TypeError: Cannot read p ...

basic file not functioning properly

So I am currently developing a straightforward script to assign the value " ...php?answer=1" only if the java is enabled. It so happens that I have made some progress on this... <script language="text/javascript"> document.form.answer.value=1; < ...

managing data transmission in Vue

Seeking assistance as a Vue beginner, I am struggling with passing props between components and could use some guidance. events.js props: ["id", "event"], defined the props data: function() { return { regular: null, e ...

saving the hash key in a separate array

Currently, I have a collection of key-value pairs that need to be stored in another array. However, I am facing difficulties with the logic as I am unable to keep track of which keys-values have already been assigned while iterating over the list of object ...

WordPress now features the ability to toggle both parent menu items when clicked, providing a more streamlined user experience

I am currently facing an issue with a menu structure, where parent menu items have submenus that need to toggle on click. However, I am encountering a problem where, upon placing the code inside a forEach loop, both submenus toggle instead of only togglin ...

Discover the method for invoking a Javascript function within a Leaflet popup using HTML

Upon clicking on a marker on the leaflet map, I aim to trigger a popup box that contains five elements: Title Description Image Button (Next Image) Button (Previous Image) To achieve this, I attempted to include a custom popup for each feature ...

Obtain the clicked href value and pass it from one page to another

Welcome, everyone, I am currently in the process of creating a webpage that serves the following functions: When the administrator accesses the page, they will see a form populated with information fetched from a database: https://i.sstatic.net/RrE9l.png ...

Adjusting canvas context position after resizing

I've been experimenting with using a canvas as a texture in three.js. Since three.js requires textures to have dimensions that are powers of two, I initially set the canvas width and height to [512, 512]. However, I want the final canvas to have non-p ...

Discovering the Harshad number

I find myself in a predicament where the issue at hand is defined as: Harshad/Niven numbers are positive numbers that are divisible by the sum of their digits. All single-digit numbers are Harshad numbers. For instance, 27 is a Harshad number as 2 + 7 = ...

Creating dynamic data fields in a Vue instance for form validation in Vue2

As a newcomer to Vue.js, I'm tackling the challenge of implementing form validation on a dynamically generated form. The input fields are populated based on the JSON object retrieved through an AJAX request. While exploring various form validation lib ...

What is the best way to expand an object without including any null values?

Imagine a scenario where there is an object: var user = { "Name": "Dan", "Age": 27, "Hobbies": null }; and it needs to be merged with the following base object to ensure all necessary properties are present: var base = { "Name": nul ...

"Strategies for disabling navigation in a React single-page application when a user manually inputs a page href with React Router v6

Our application is built with Single Page Application (SPA) architecture and we utilize react router v6 for handling navigation. We have a scenario where each page has a specific set of restricted URLs which users should not be able to access directly by ...

Struggling to get troika-three-text installed via npm?

I'm having trouble integrating Troika with my three-js scene. While others seem to have no issue, I am struggling to call the module and encountering problems with references. It's frustrating because I can't seem to find any resources to he ...

Tips for creating a unique exception in AngularJS?

I have a customException.js script with the following service: app.service('CustomException', function() { this.CustomException1 = function (message) { if (!message) { message = "Custom Exception 1 occurred!"; } return { ...

Having difficulty locating the value in the input search field

I am attempting to customize my search bar so that it automatically includes wildcards on both ends when a user performs a search. Although the code below almost does what I want by adding the wildcards on both sides, it adds them to an empty search term ...

The act of appending values to an array within a hash in Vue is not functioning as expected

I am currently working on implementing a feature that allows users to add multiple workers by clicking the "Add worker" button. However, I have encountered an issue where placing the workers array inside the management object prevents this feature from f ...

Scrolling horizontally in a container using the mouse wheel

Is there a way to enable horizontal scrolling in a div using the mouse wheel or drag functionality with jQuery? I attempted using draggable, but it did not work effectively in my specific code scenario. Currently, I have a horizontal scrollbar. Are there ...

The functionality of JQuery's .hover() is disabled once an HTML onClick() event is activated

I am currently working on a webpage and attempting to incorporate JQuery into it for the first time. However, I seem to be encountering some issues that I believe might have simple solutions. Background Information My JQuery code only contains one event l ...