Frontend Performance Tuning: Optimizing JavaScript Arrays and Objects for Better Code Efficiency

Photo by Chris Ried on Unsplash

Frontend Performance Tuning: Optimizing JavaScript Arrays and Objects for Better Code Efficiency

In the process of writing and maintaining front-end code, we often encounter scenarios that require performance optimization. Particularly when handling arrays and objects, the appropriate use of JavaScript methods can not only improve the execution efficiency of the code but also make it more concise and easier to understand. In this article, I will take you through a specific permission validation logic and discuss how to achieve optimization by leveraging JavaScript's array and collection methods.

Business Scenario

Let's first look at a common scenario where we need to verify whether users have been assigned the appropriate permissions for each selected permission scope. This requirement sounds very straightforward, but in practical implementation, it may involve a series of array operations, and that is the source code we want to optimize today:

// Initial permission validation logic
const selectedObjItem = [];
this.selectedPermissions.forEach((item) => {
  !selectedObjItem.includes(item.action_id) && selectedObjItem.push(item.action_id);
});

const result = this.selectedScopes.every(scope => {
  if (scope.has_instance) {
    return selectedObjItem.includes(scope.action_id);
  }
  return true;
});

if (!result) {
  showMsg(this.$t('AUTH:Please select the object scope'), 'warning');
  return false;
}

The purpose of this code is to validate whether each selected scenario (selectedScopes) has selected a permission scope, and these permission scopes need to be matched in selectedPermissions (each permission object has a unique identifier action_id).

Although the code superficially achieves the expected effect, detailed analysis reveals that there are still opportunities for optimization. Indeed, we can improve execution efficiency and make the code clearer with a few simple changes. For this, we can consider the following questions:

  1. To achieve array recording and deduplication, is there a more efficient way?

  2. Is every the best method for traversing and validating choices?

  3. For the final result judgment, is it necessary to wait until all traversals have ended?

Optimization

1. Deduplication and Recording

The first step of recording actually includes deduplication, mainly in the pre-push proof determination: !selectedObjItem.includes(item.action_id). As long as deduplication is involved, we can think of using ES6's naturally efficient data type Set, as Set inherently has deduplication properties, so the code can be optimized as:

const selectedObjItem = new Set();
this.selectedPermissions.forEach((item) => {
  selectedObjItem.add(item.action_id);
});

Using Set for deduplication and combining it with the has method to check for element existence is generally more efficient than using arrays for deduplication (using push in conjunction with includes). The reasons are as follows:

  1. Time Complexity:

    • The add and has methods of a Set usually have a constant time complexity O(1), as Set is implemented internally by a hash table, which allows for fast data lookup and insertion.

    • The includes method of an array has a time complexity of O(n) because, in the worst-case scenario, it needs to traverse the entire array to determine whether an element exists.

  2. Deduplication:

    • Set is designed as a collection of unique elements; it automatically manages the uniqueness of its elements. When trying to add an element already in the Set, the Set will not perform any operation, so there is no need to manually write deduplication logic.

    • To deduplicate an array, additional logic (usually using includes then push) is required to ensure the elements are unique, increasing code complexity and operational steps.

  3. Code Conciseness:

    • Using Set makes the code more concise and understandable. As the Set interface clearly represents a collection data structure, it makes the intent of the code clearer and semantically better.

    • Using arrays for deduplication involves the includes and push methods. Although these methods are array methods and easy to understand, they need to be combined to achieve deduplication, which makes the code less intuitive.

The use and application of Set are further introduced in [the following section](##Appendix: Description and Examples of Set)

2. Checking Optimization

It's possible to use the every method in the source code, but it's also inefficient because every needs to traverse all items. Since the purpose of the check is to ensure all items are selected, if one is not selected, we can return an error result early without needing to check the rest.

With this thought, it's easy to think of the some method, and the optimized code is as follows:

const result = this.selectedScopes.some(scope => {
  if (scope.has_instance) {
    return !selectedObjItem.has(scope.action_id);
  }
  return false;
});

The logic and structure of the code look no different from the original, but the actual efficiency will improve since some will not continue subsequent loops after returning true.

3. Complete Code

The complete optimized code is as follows:

// Deduplicate and record action_id
const selectedObjItem = new Set();
this.selectedPermissions.forEach((item) => {
  selectedObjItem.add(item.action_id);
});

// Check if all options have been selected
const result = this.selectedScopes.some(scope => {
  if (scope.has_instance) {
    return !selectedObjItem.has(scope.action_id);
  }
  return false;
});

if (result) {
  showMsg(this.$t('AUTH:Please select the object scope'), 'warning');
  return false;
}

Summary

JavaScript is an ever-evolving language. The ES6 version introduced new syntax and features that greatly improved the efficiency and readability of code writing. Here's a brief summary of the methods and applications mentioned:

  1. Use the new data structure Set**: Implementing array deduplication and efficient recording of non-duplicate values is very convenient. Compared to traditional traversal checking methods, Set's native mechanism ensures the collection's uniqueness, significantly impacting performance optimization, particularly in handling large datasets.

  2. Rational use of array methods: ES6 offers array methods such as forEach, every, and some. Appropriate use of these methods can make code clearer and choose the most suitable iteration method for different business logic needs, such as using some to return early results, avoiding unnecessary traversal.

  3. Optimizing logical judgment: Logical optimizations such as looping early can save resources and time by avoiding unnecessary computations. When validating arrays or collections, considering boundary conditions and short-circuit logic can lead to more efficient code execution.

  4. Utilizing ES6's concise syntax: Features such as arrow functions and template strings can make the code more concise, avoid redundancy, and enhance code readability.

In practical programming, each optimization point may only bring a slight performance enhancement, but collectively, these improvements can significantly increase application performance and user experience. By incorporating the above optimization methods, we can write JavaScript code that is both efficient and readable.

Appendix: Description and Examples of Set

Set is a special type of collection — "a collection of values" (without keys), and each value can only occur once.

The previous section introduced one application scenario of Set. Here is a simple review of the features of this data type. Its main methods are as follows:

  • new Set(iterable) — creates a set; if an iterable object (usually an array) is provided, values from the array will be copied into the set.

  • set.add(value) — adds a value, returns the set itself.

  • set.delete(value) — removes the value; returns true if value is present at the time of the call, otherwise returns false.

  • set.has(value) — if value is in the set, returns true; otherwise returns false.

  • set.clear() — clears the set.

  • set.size — returns the number of elements.

Its main characteristic is that repeating the same value call set.add(value) does not bring about any change, which is why each value in Set appears only once.

Counting (Statistics)

First, look at the following code to understand what it's doing and the associated principle without comments:

const uid = () =>
  String(
    Date.now().toString(32) +
      Math.random().toString(16)
  ).replace(/\./g, '');

const size = 1000000
const set = new Set(new Array(size)
  .fill(0)
  .map(() => uid()))

console.log(
  size === set.size ? 'all ids are unique' : `not unique records ${size - set.size}`
)

Correct, it's actually testing the uid function to check whether the generated id is unique. Interestingly, here is how Set is used to make a uniqueness judgment: why can set.size === size determine whether the created uuids are unique?

It's precisely because each value in a Set collection can only occur once; therefore, when using map with uid() to generate values, any identical values will not be added to the collection. Using this feature, we can count whether there are duplicate values at the beginning of the example and how many duplicates there are:

  • By accessing set.size, if all are unique, then the length of the collection after mapping is consistent with the initially defined size;

  • Otherwise, the difference between size and set.size is the number of duplicates.

Reasonably using Set can not only make the code more efficient but also refines and simplifies the understanding of the code, so in scenarios such as deduplication, statistics, counting, etc., if you previously used arrays without hesitation or consideration, you might want to pay more attention to whether you could use Set to improve code efficiency.