Select Page

Exploring Randomness In JavaScript

Ben Nadel
Published: June 22, 2024

In my post yesterday, on building a color palette utility in Alpine.js, randomness played a big part: each swatch was generated as a composite of randomly selected Hue (0..360), Saturation (0..100), and Lightness (0..100) values. As I was putting that demo together, I came across the Web Crypto API. Normally, when generating random values, I use the the Math.random() method; but, the MDN docs mention that Crypto.getRandomValues() is more secure. As such, I ended up trying Crypto out (with a fallback to the Math module as needed). But, this left me wondering if “more secure” actually meant “more random” for my particular use-case.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Randomness, from a security standpoint, has an actual meaning. I’m not a security expert; but, my understanding is that when a pseudo-random number generator (PRNG) is considered “secure”, it means that the sequence of numbers that it will produce—or has already produced—cannot be deduces by an attacker.

When it comes to “random color generators”, like my color palette utility, the notion of “randomness” is much more fuzzy. In my case, the random color generation is only as random as is “feels” to the user. In other words, the effectiveness of the randomness is part of the overall user experience (UX).

To this end, I want to try generating some random visual elements using both Math.random() and crypto.getRandomValues() to see if one of the methods feels substantively different. Each trial will contain a randomly generated element and a randomly generated set of integers. Then, I will use my (deeply flawed) human intuition to see if one of the trials looks “better” than the other.

The Math.random() method works by returning a decimal value between 0 (inclusive) and 1 (exclusive). This can be used to generate random integers by taking the result of the randomness and multiplying it against a range of possible values.

In other words, if Math.random() returned 0.25, you’d pick the value that lands closest to 25% along the given min-max range. And, if Math.random() returned 0.97, you’d pick the value that lands closest to 97% along the given min-max range.

The crypto.getRandomValues() method works very differently. Instead of returning a single value, you pass-in a Typed Array with a pre-allocated size (length). The .getRandomValues() method then fills that array with random values dictated by the min/max values that can be stored by the given Type.

To make this exploration easier, I want both approaches to work roughly the same. So, instead of having to deal with decimals in one algorithm and integers in another algorithm, I’m going to force both algorithms to rely on decimal generation. Which means, I have to coerce the value returned by .getRandomValues() into a decimal (0..1):

value / ( maxValue + 1 )

I’ll encapsulate this difference in two method, randFloatWithMath() and randFloatWithCrypto():

/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Math module.
*/
function randFloatWithMath() {
	return Math.random();
}
/**
* I return a random float between 0 (inclusive) and 1 (exclusive) using the Crypto module.
*/
function randFloatWithCrypto() {
	var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
	var maxInt = 4294967295;
	return ( randomInt / ( maxInt + 1 ) );
}

With these two methods in place, I can then assign one of them to a randFloat() reference which can be used to seamless generate random values along a given range using either algorithm interchangeably:

/**
* I generate a random integer in between the given min and max, inclusive.
*/
function randRange( min, max ) {
	return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );
}

Now to create the experiment. The user interface is small and is powered by Alpine.js. Each trial uses the same Alpine.js component; but, its constructor receives an argument that determines which randFloat() implementation will be used:



	
	
	

	
	

Math Module

Duration:

Crypto Module

Duration: ms