JavaScript's Math.random()

Image by Nick Hillier.

Back in 2014, I discussed how we can access random elements in an array through JavaScript, by making use of the Math.random() function. This function returns a random number from 0 inclusive to 1 exclusive, and we can scale that value to choose a random index in our arrays. The returned number is not the generator's seed (you can read this post here).

I've recently been implementing a lot of randomisation into one of my projects, which got me thinking: just how random is Math.random()? How does it work?


How Random is Random?

As you are probably well aware, when it comes to computing, we are ultimately talking about maths. On the absolute most rudimentary level, computers take an input, math it up, and then return the result. So, how does that affect our concept of "random", then? Given that it's all just an input with some logic applied, how random can it really be?

If we want to be really specific about it; because computers use an algorithm that in turn uses mathematical formulas to generate 'random' numbers, we aren't talking about a true Random Number Generator (RNG) at all, but more actually: a PseudoRandom Number Generator (PRNG) instead.

There are a few services out there that you can use if you want to generate truly random values rather than using PRNGs. One such service is RANDOM.ORG which uses atmospheric noise instead of PRNGs to produce random numbers. The results of using an RNG are much more randomised, are not periodic (this is a measure of how many times you would have to run an RNG or PRNG before they inevitably start repeating themselves and showing patterns), and are much better suited to things like lotteries, gambling or encryption algorithms (due to their unpredictable nature).

However, the biggest downside of RNGs is that they are noticeably less efficient. Whilst normal PRNGs employed by computers take fractions of fractions of a second to generate a randomised result, TRNGs (True Random Number Generators) like those found on random.org take time. This makes them much less useful for things like simulations, or for use in the kind of work most developers and specifically web developers will be running into on a daytoday basis.


The Mystery of JavaScript's PRNG

So, we know that computers use algorithms in PRNGs to create the illusion of randomness. Different languages use different algorithms and PRNGs. So what does JavaScript use? This is where it gets interesting: ECMAScript defines Math.random(), but deliberately leaves its algorithm or strategy to the implementation.

What? How is that possible? How can Math.random() work without JavaScript having any way to generate those random numbers?

A JavaScript engine supplies the implementation behind Math.random(). That is not limited to browsers: serverside and other runtimes implement the same ECMAScript API too. The strategy can differ between engines and between releases of the same engine.


2015: Unifying the Browsers

Until late 2015, V8 used MWC1616. Firefox and Safari also had their own engine implementations, as the ECMAScript specification allowed.

Between late 2015 and 2016, V8, Firefox, and Safari moved to xorshift128+, whilst Chromiumbased Opera inherited V8. This was broad engine convergence, not an ECMAScript requirement, and it did not guarantee that every JavaScript runtime used the same strategy.


Xorshift128+

The mass switchover to xorshift128+ was a good move. Xorshift is a faster, more efficient PRNG than all of the previous ones that were in use by the mainstream browsers (and it must never be used to generate cryptographic keys, security tokens, or other securitysensitive values).

To understand generally how it works, we're going to have to get uncomfortably close to bits and binary; something that as web developers we don't tend to have to think about all that often.

According to the V8 dev blog, the engine implementation was illustrated with this C++ code:

uint64_t state0 = 1;uint64_t state1 = 2;uint64_t xorshift128plus() {  uint64_t s1 = state0;  uint64_t s0 = state1;  state0 = s0;  s1 ^= s1 << 23;  s1 ^= s1 >> 17;  s1 ^= s0;  s1 ^= s0 >> 26;  state1 = s1;  return state0 + state1;}

So, as a developer this shouldn't look totally alien to you; I'm sure you have seen variable assignments, functions and return statements before, and you're probably also familiar with binary logical operators like the double ampersand. However, the operators that look like binary logical operators here (the >> and <<) are actually serving an entirely different purpose.

Bitwise Operators

The << and >> operators here are called bitwise operators. Specifically, they are the "leftshift" and "rightshift" operators (hopefully it's pretty obvious which one is which!).

Basically, they are taking a number, changing it into binary, and shifting the bits in the direction the arrow is pointing by however many places they are told to. For example, 17 << 9 tells the program to take the binary representation of 17, and shift each bit to the left by 9 places. This results in a drastically different number than the original. In fact, running 17 << 9 results in that 17 becoming 8704!

However, there is another step at play here the ^= operator. The ^= operator is called the exclusive or assignment operator (also known as the XOR assignment). To illustrate how this works, we'll drop the equals sign and just use it as the Xor operator on its own. Here's what's going on under the hood: 24 ^ 11 tells the program to take the binary representations of both 24 and 11, and compare the positions of the bits. Where the bits match, Xor outputs a 0. Where they don't, it outputs a 1. This results in an entirely new number. For instance, with the code 24 ^ 11, we would end up with the number 19.

Now that we know a little more about what's going on, let's break down a single line. The line, s1 ^= s1 >> 17; takes the value of s1 and rightshifts it by 17 places, then Xors it against the previous value of s1, and then updates s1 with that new value. Perhaps this helps to shed some line on why it's called Xorshift?

Taking in the wider function, the two values shown are internal state, not arguments or callerprovided seeds. The function shifts and XORs that state, updates it, and returns a value derived from the new state.

This is extremely efficient and results in a relatively random generated number too.


The Wrap‑Up

A deterministic PRNG sequence will eventually repeat because its internal state is finite. That does not mean every random value available to a program is necessarily deterministic: operating systems and hardware can provide additional entropy sources.

Math.random() is designed for convenient, fast, nonsecurity randomness. It is not cryptographically secure. Browser code that needs securitysensitive random values should use crypto.getRandomValues(), whose implementation draws on the platform's cryptographic random source.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.