【他山之石】优化 JavaScript 的乐趣与价值(下)

前言
继本文的 上篇 发表之后,没想到反响还挺好,看来大家在 JS 优化的问题上越来越注重“与国际接轨”了。一起来看本文的下篇,也是干货满满。

文章目录

    • 6. Avoid large objects
        • What the eff should I do about this?
    • 7. Use eval
    • 8. Use strings, carefully
        • What the eff should I do about this?
            • On strings complexity
    • 9. Use specialization
            • Branch prediction and branchless code
    • 10. Data structures
    • 11. Benchmarking
      • 11.0 Start with the top
      • 11.1 Avoid micro-benchmarks
      • 11.2 Doubt your results
      • 11.3 Pick your target
    • 12. Profiling & tools
      • 12.1 Browser gotchas
      • 12.2 Sample vs structural profiling
      • 12.3 The tools of the trade
    • Final notes

6. Avoid large objects

As explained in section 2, engines use shapes to optimize objects. However, when the shape grows too large, the engine has no choice but to use a regular hashmap (like a Map object). And as we saw in section 5, cache misses decrease performance significantly. Hashmaps are prone to this because their data is usually randomly & evenly distributed over the memory region they occupy. Let’s see how it behaves with this map of some users indexed by their ID.

// setup:
const USERS_LENGTH = 1_000
// setup:
const byId = {}
Array.from({ length: USERS_LENGTH }).forEach((_, id) => {byId[id] = { id, name: 'John'}
})
let _ = 0
// 1. [] access
Object.keys(byId).forEach(id => { _ += byId[id].id })
// 2. direct access
Object.values(byId).forEach(user => { _ += user.id })

benchmark8

And we can also observe how the performance keeps degrading as the object size grows:

// setup:
const USERS_LENGTH = 100_000

benchmark9

What the eff should I do about this?

As demonstrated above, avoid having to frequently index into large objects. Prefer turning the object into an array beforehand. Organizing your data to have the ID on the model can help, as you can use Object.values() and not have to refer to the key map to get the ID.

7. Use eval

Some javascript patterns are hard to optimize for engines, and by using eval() or its derivatives you can make those patterns disappear. In this example, we can observe how using eval() avoids the cost of creating an object with a dynamic object key:

// setup:
const key = 'requestId'
const values = Array.from({ length: 100_000 }).fill(42)
// 1. without eval
function createMessages(key, values) {const messages = []for (let i = 0; i < values.length; i++) {messages.push({ [key]: values[i] })}return messages
}createMessages(key, values)
// 2. with eval
function createMessages(key, values) {const messages = []const createMessage = new Function('value',`return { ${JSON.stringify(key)}: value }`)for (let i = 0; i < values.length; i++) {messages.push(createMessage(values[i]))}return messages
}createMessages(key, values)

benchmark10

Another good use-case for eval could be to compile a filter predicate function where you discard the branches that you know will never be taken. In general, any function that is going to be run in a very hot loop is a good candidate for this kind of optimization.

Obviously the usual warnings about eval() apply: don’t trust user input, sanitize anything that gets passed into the eval()‘d code, and don’t create any XSS possibility. Also note that some environments don’t allow access to eval(), such as browser pages with a CSP.

8. Use strings, carefully

We’ve already seen above how strings are more expensive than they appear. Well I have kind of a good news/bad news situation here, which I’ll announce in the only logical order (bad first, good second): strings are more complex than they appear, but they can also be quite efficient used well.

String operations are a core part of JavaScript due to its context. To optimize string-heavy code, engines had to be creative. And by that I mean, they had to represent the String object with multiple string representation in C++, depending on the use case. There are two general cases you should worry about, because they hold true for V8 (the most common engine by far), and generally also in other engines.

First, strings concatenated with + don’t create a copy of the two input strings. The operation creates a pointer to each substring. If it was in typescript, it would be something like this:

class String {abstract value(): char[] {}
}class BytesString {constructor(bytes: char[]) {this.bytes = bytes}value() {return this.bytes}
}class ConcatenatedString {constructor(left: String, right: String) {this.left = leftthis.right = right}value() {return [...this.left.value(), ...this.right.value()]}
}function concat(left, right) {return new ConcatenatedString(left, right)
}const first = new BytesString(['H', 'e', 'l', 'l', 'o', ' '])
const second = new BytesString(['w', 'o', 'r', 'l', 'd'])// See ma, no array copies!
const message = concat(first, second)

Second, string slices also don’t need to create copies: they can simply point to a range in another string. To continue with the example above:

class SlicedString {constructor(source: String, start: number, end: number) {this.source = sourcethis.start = startthis.end = end}value() {return this.source.value().slice(this.start, this.end)}
}function substring(source, start, end) {return new SlicedString(source, start, end)
}// This represents "He", but it still contains no array copies.
// It's a SlicedString to a ConcatenatedString to two BytesString
const firstTwoLetters = substring(message, 0, 2)

But here’s the issue: once you need to start mutating those bytes, that’s the moment you start paying copy costs. Let’s say we go back to our String class and try to add a .trimEnd method:

class String {abstract value(): char[] {}trimEnd() {// `.value()` here might be calling// our Sliced->Concatenated->2*Bytes string!const bytes = this.value()const result = bytes.slice()while (result[result.length - 1] === ' ')result.pop()return new BytesString(result)}
}

So let’s jump to an example where we compare using operations that use mutation versus only using concatenation:

// setup:
const classNames = ['primary', 'selected', 'active', 'medium']
// 1. mutation
const result =classNames.map(c => `button--${c}`).join(' ')
// 2. concatenation
const result =classNames.map(c => 'button--' + c).reduce((acc, c) => acc + ' ' + c, '')

benchmark11

What the eff should I do about this?

In general, try to avoid mutation for as long as possible. This includes methods such as .trim(), .replace(), etc. Consider how you can avoid those methods. In some engines, string templates can also be slower than +. In V8 at the moment it’s the case, but might not be in the future so as always, benchmark.

A note on SlicedString above, you should note that if a small substring to a very large string is alive in memory, it might prevent the garbage collector from collecting the large string! If you’re processing large texts and extracting small strings from it, you might be leaking large amounts of memory.

const large = Array.from({ length: 10_000 }).map(() => 'string').join('')
const small = large.slice(0, 50)
//    ^ will keep `large` alive

The solution here is to use mutation methods to our advantage. If we use one of them on small, it will force a copy, and the old pointer to large will be lost:

// replace a token that doesn't exist
const small = small.replace('#'.repeat(small.length + 1), '')

For more details, see string.h on V8 or JSString.h on JavaScriptCore.

On strings complexity

I have skimmed very quickly over things, but there are a lot of implementation details that add complexity to strings. There are often minimum lengths for each of those string representations. For example a concatenated string might not be used for very small strings. Or sometimes there are limits, for example avoiding pointing to a substring of a substring. Reading the C++ files linked above gives a good overview of the implementation details, even if just reading the comments.

9. Use specialization

One important concept in performance optimization is specialization: adapting your logic to fit in the constraints of your particular use-case. This usually means figuring out what conditions are likely to be true for your case, and coding for those conditions.

Let’s say we are a merchant that sometimes needs to add tags to their product list. We know from experience that our tags are usually empty. Knowing that information, we can specialize our function for that case:

// setup:
const descriptions = ['apples', 'oranges', 'bananas', 'seven']
const someTags = {apples: '::promotion::',
}
const noTags = {}// Turn the products into a string, with their tags if applicable
function productsToString(description, tags) {let result = ''description.forEach(product => {result += productif (tags[product]) result += tags[product]result += ', '})return result
}// Specialize it now
function productsToStringSpecialized(description, tags) {// We know that `tags` is likely to be empty, so we check// once ahead of time, and then we can remove the `if` check// from the inner loopif (isEmpty(tags)) {let result = ''description.forEach(product => {result += product + ', '})return result} else {let result = ''description.forEach(product => {result += productif (tags[product]) result += tags[product]result += ', '})return result}
}
function isEmpty(o) { for (let _ in o) { return false } return true }
// 1. not specialized
for (let i = 0; i < 100; i++) {productsToString(descriptions, someTags)productsToString(descriptions, noTags)productsToString(descriptions, noTags)productsToString(descriptions, noTags)productsToString(descriptions, noTags)
}
// 2. specialized
for (let i = 0; i < 100; i++) {productsToStringSpecialized(descriptions, someTags)productsToStringSpecialized(descriptions, noTags)productsToStringSpecialized(descriptions, noTags)productsToStringSpecialized(descriptions, noTags)productsToStringSpecialized(descriptions, noTags)
}

benchmark12

This sort of optimization can give you moderate improvements, but those will add up. They are a nice addition to more crucial optimizations, like shapes and memory I/O. But note that specialization can turn against you if your conditions change, so be careful when applying this one.

Branch prediction and branchless code

Removing branches from your code can be incredibly efficient for performance. For more details on what a branch predictor is, read the classic Stack Overflow answer Why is processing a sorted array faster.

10. Data structures

I won’t go in details about data structures as they would require their own post. But be aware that using the incorrect data structures for your use-case can have a bigger impact than any of the optimizations above. I would suggest you to be familiar with the native ones like Map and Set, and to learn about linked lists, priority queues, trees (RB and B+) and tries.

But for a quick example, let’s compare how Array.includes does against Set.has for a small list:

// setup:
const userIds = Array.from({ length: 1_000 }).map((_, i) => i)
const adminIdsArray = userIds.slice(0, 10)
const adminIdsSet = new Set(adminIdsArray)
// 1. Array
let _ = 0
for (let i = 0; i < userIds.length; i++) {if (adminIdsArray.includes(userIds[i])) { _ += 1 }
}
// 2. Set
let _ = 0
for (let i = 0; i < userIds.length; i++) {if (adminIdsSet.has(userIds[i])) { _ += 1 }
}

benchmark13

As you can see, the data structure choice makes a very impactful difference.

As a real-world example, I had a case where we were able to reduce the runtime of a function from 5 seconds to 22 milliseconds by switching out an array with a linked list.

11. Benchmarking

I’ve left this section for the end for one reason: I needed to establish credibility with the fun sections above. Now that I (hopefully) have it, let me tell you that benchmarking is the most important part of optimization. Not only is it the most important, but it’s also hard. Even after 20 years of experience, I still sometimes create benchmarks that are flawed, or use the profiling tools incorrectly. So whatever you do, please put the most effort into benchmarking correctly.

11.0 Start with the top

Your priority should always be to optimize the function/section of code that makes up the biggest part of your runtime. If you spend time optimizing anything else than the top, you are wasting time.

11.1 Avoid micro-benchmarks

Run your code in production mode and base your optimizations on those observations. JS engines are very complex, and will often behave differently in micro-benchmarks than in real-world scenarios. For example, take this micro-benchmark:

const a = { type: 'div', count: 5, }
const b = { type: 'span', count: 10 }function typeEquals(a, b) {return a.type === b.type
}for (let i = 0; i < 100_000; i++) {typeEquals(a, b)
}

If you’ve payed attention sooner, you will realize that the engine will specialize the function for the shape { type: string, count: number }. But does that hold true in your real-world use-case? Are a and b always of that shape, or will you receive any kind of shape? If you receive many shapes in production, this function will behave differently then.

11.2 Doubt your results

If you’ve just optimized a function and it now runs 100x faster, doubt it. Try to disprove your results, try it in production mode, throw stuff at it. Similarly, doubt also your tools. The mere fact of observing a benchmark with devtools can modify its behavior.

11.3 Pick your target

Different engines will optimize certain patterns better or worse than others. You should benchmark for the engine(s) that are relevant to you, and prioritize which one is more important. Here’s a real-world example in Babel where improving V8 means decreasing JSC’s performance.

12. Profiling & tools

Various remarks about profiling and devtools.

12.1 Browser gotchas

If you’re profiling in the browser, make sure you use a clean and empty browser profile. I even use a separate browser for this. If you’re profiling and you have browser extensions enabled, they can mess up the measurements. React devtools in particular will substantially affect results, rendering code may appear slower than it appears in the mirror to your users.

12.2 Sample vs structural profiling

Browser profiling tools are sample-based profilers, which take a sample of your stack at regular intervals. This had a big disadvantage: very small but very frequent functions might be called between those samples, and might be very much underreported in the stack charts you’ll get. Use Firefox devtools with a custom sample interval or Chrome devtools with CPU throttling to mitigate this issue.

12.3 The tools of the trade

Beyond the regular browser devtools, it may help to be aware of these options:

  • Chrome devtools have quite a few experimental flags that can help you figure out why things are slow. The style invalidation tracker is invaluable when you need to debug style/layout recalculations in the browser.
    https://github.com/iamakulov/devtools-perf-features
  • The deoptexplorer-vscode extension allows you to load V8/chromium log files to understand when your code is triggering deoptimizations, such as when you pass different shapes to a function. You don’t need the extension to read log files, but it makes the experience much more pleasant.
    https://github.com/microsoft/deoptexplorer-vscode
  • You can always compile the debug shell for each JS engine to understand more in details how it works. This allows you to run perf and other low-level tools, and also to inspect the bytecode and machine code generated by each engine.
    Example for V8 | Example for JSC | Example for SpiderMonkey (missing)

Final notes

Hope you learned some useful tricks. If you have any comments, corrections or questions, email in the footer. I’m always happy to receive feedback or questions from readers.



From: https://romgrk.com/posts/optimizing-javascript

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/143418.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Linux用户账号管理

目录 一、useradd 创建新用户 二、usermod 修改用户账号 三、userdel 删除用户账号 四、passwd 设置或更改用户密码 五、who 或 w 查看当前登录用户 六、切换用户 6.1. su命令切换用户 6.2. sudo授权命令 6.2.1. sudo的特性 6.2.2. sudo的相关文件 6.3. exit退出 6…

自制数据库迁移工具-C版-04-HappySunshineV1.4-(支持Gbase8a、PG)

目录 一、环境信息 二、简述 三、架构图 四、升级点 五、支持功能 六、安装包下载地址 七、配置参数介绍 八、安装步骤 1、配置环境变量 2、生效环境变量 3、检验动态链接是否正常 4、修改配置文件MigrationConfig.txt &#xff08;1&#xff09;Gbase8a -> Gba…

Axios基本语法和前后端交互

Axios是一个js框架&#xff0c;用于发送ajax请求。 一、导入 // node中&#xff0c;使用npm安装 npm install axios // HTML中&#xff0c;使用cdn安装 <script src"https://unpkg.com/axios/dist/axios.min.js"></script> 二、基本使用 // 使用axios…

x264中的cabac编码实现

typedef struct { /* state */ int i_low; //概率状态的范围low int i_range; //当前概率状态 范围range /* bit stream */ int i_queue; //stored with an offset of -8 for faster asm 队列中可输出的bits 个数&#xff0c;-8 开始&#xff0c;是为了方便asm优化 int i_byt…

数据防泄密系统的构建与功能分析(实用物料)

一、构建1、需求分析&#xff1a;明确企业需要保护的敏感数据类型&#xff08;如商业机密、研发资料等&#xff09;及其潜在的泄露途径&#xff08;如网络传输、文件共享、打印复印等&#xff09;。 2、策略&#xff1a;根据需求分析结果&#xff0c;制定详细的数据防泄密策略…

数字逻辑电路-加法器

目录 半加器和全加器 半加器 ​全加器 集成全加器 利用全加器实现二进制的乘法功能 加法器 半加器和全加器 半加器 不考虑低位进位的加法。 本位为s&#xff0c;进位为c。 全加器 多了一个相邻低位来的进位数。 集成全加器 左上角和右下角那两个是不用的。 利用全加器…

Selenium通过ActionBuilder模拟鼠标操作直接移动到指定坐标的注意事项

在目前&#xff08;2024-09-18&#xff09;得Selenium官方手册中&#xff0c;模拟鼠标操作基本上都是通过ActionChains完成的&#xff0c;唯独有一动作&#xff0c;是通过ActionBuilder完成的。 而前者ActionChains&#xff0c;主要是通过offset&#xff0c;也就是坐标偏移量来…

RK3568笔记五十九:FastSAM部署

若该文为原创文章,转载请注明原文出处。 记录FastSAM训练到部署全过程,转换模型和yolov8一样。 一、介绍 Fast Segment Anything Model (FastSAM) 是一种基于 CNN 的新型实时解决方案,可用于 Segment Anything 任务。该任务旨在根据各种可能的用户交互提示分割图像中的任何…

AT24CXX系列eeprom的相关知识总结

常用的eeprom存储器件有很多容量类型&#xff0c;AT系列的eeprom有at24c01,at24c02…at24c1024等。我们来做一个总结。 1.常见的型号含义 at24c01&#xff1a;表示1kbit&#xff08;128BYTE*8&#xff09; at24c02&#xff1a;表示2kbit&#xff08;256BYTE*8&#xff09; . .…

pybind11 学习笔记

pybind11 学习笔记 0. 一个例子1. 官方文档1.1 Installing the Library1.1.1 Include as A Submodule1.1.2 Include with PyPI1.1.3 Include with Conda-forge 1.2 First Steps1.2.1 Separate Files1.2.2 PYBIND11_MODULE() 宏1.2.3 example.cpython-38-x86_64-linux-gnu.so 的…

二百六十四、Java——Java采集Kafka主题A的JSON数据,解析成一条条数据,然后写入Kafka主题B中

一、目的 由于Hive是单机环境&#xff0c;因此庞大的原始JSON数据在Hive中解析的话就太慢了&#xff0c;必须放在Hive之前解析成一个个字段、一条条CSV数据 二、IDEA创建SpringBoot项目 三、项目中各个文件 3.1 pom.xml <?xml version"1.0" encoding"UTF…

java: 警告: 源发行版 17 需要目标发行版 17(100% 解决)

1. 问题说明 Idea启动Springboot服务报错&#xff1a;java: 警告: 源发行版 17 需要目标发行版 17 2. 解决方案 Project Structure指定jdk版本为我们当前使用的版本&#xff1b; Java Compiler指定jdk为我们当前使用的版本&#xff1b; Invalidate Caches重启Idea。 如果还…

小商品市场配电系统安全用电解决方案

1.概述 随着市场经济的快速发展和人民生活水平的不断提高,全国各地相继建起了大批大型小商品批发市场,此类市场以其商品种类繁多、价格实惠、停车方便等特点吸引了大量的顾客,成为人们日常光顾的重要场所,地方便了广大人民群众的日常生活。 小商品市场集商品销售和短时货物储…

如何利用生成式AI创建图像和可视化效果

每个小型出版商在创建博客文章或新闻文章的过程中&#xff0c;都有一个恐慌时刻&#xff1a; “我用什么做我的特色图片&#xff1f;” 广告公司和媒体公司都有创意总监、摄影师和艺术家随时为他们创作图片。但我们其他人怎么办呢&#xff1f; 我们中的一些人会不顾更好的判…

数据中心扩展之路:创新的数据中心布线解决方案

在不断发展的数据管理领域中&#xff0c;现代技术的迅猛发展既带来了机遇&#xff0c;也带来了挑战&#xff0c;尤其是对不断扩展的数据中心而言。随着这些基础设施的快速发展和转型&#xff0c;对高效可靠的数据中心布线解决方案的需求日益增长。本文将探讨飞速&#xff08;FS…

redis常见类型设置、获取键值的基础命令

redis常见类型设置、获取键值的基础命令 获取键值的数据类型 命令&#xff1a;TYPE keyname 常见数据类型设置、获取键值的基本命令 string类型 置键值&#xff1a;set keyname valuename获取键值&#xff1a;get keyname删除&#xff1a; del keyname list类型 从左边向列表…

关于在Qlabel遮罩方面的踩坑实录

先看目标效果&#xff1a; 想要实现封面图标的遮罩效果&#xff0c;有两个思路&#xff1a; 一、在鼠标移动到这个item上面时&#xff0c;重新绘制pixmap 例如以下代码&#xff1a; #include <QApplication> #include <QWidget> #include <QPixmap> #incl…

马尔科夫蒙特卡洛_吉布斯抽样算法(Markov Chain Monte Carlo(MCMC)_Gibbs Sampling)

定义 输入:目标概率分布的密度函数 p ( x ) p(x) p(x),函数 f ( x ) f(x) f(x) 输出: p ( x ) p(x) p(x)的随机样本 x m 1 , x m 2 , ⋯ , x n x_{m1},x_{m2},\cdots,x_n xm1​,xm2​,⋯,xn​,函数样本均值 f m n f_{mn} fmn​; 参数:收敛步数 m m m,迭代步数 n n n。 (1)初…

camtasia2024绿色免费安装包win+mac下载含2024最新激活密钥

Hey, hey, hey&#xff01;亲爱的各位小伙伴&#xff0c;今天我要给大家带来的是Camtasia2024中文版本&#xff0c;这款软件简直是视频制作爱好者的福音啊&#xff01; camtasia2024绿色免费安装包winmac下载&#xff0c;点击链接即可保存。 先说说这个版本新加的功能吧&#…

小微金融企业系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;贷款信息管理&#xff0c;贷款申请管理&#xff0c;贷款种类管理&#xff0c;代办项目管理&#xff0c;项目分类管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;代办项…