Exploring the Randomness Factor in JavaScript

In a recent post I shared how write a utility for working with a palette in Alpine.jsand an important part of this work was to program randomness: each sample on the palette was generated as a set of randomly taken values ​​of Hue (0..360), Saturation (0..100) and Lightness (0..100). While putting this demo together, I came across Web Crypto API. I usually use the Math.random() method to generate random values; but the MDN documentation says that there is a more secure method called Crypto.getRandomValues(). So I decided to give it a try. Crypto (leaving the module Math (as a backup option). But in the end, I was left wondering if there might be specific practical cases in which “increased security” means “increased randomness.”

You can do this demo as part of my project JavaScript Demos on GitHub.

View this code as part of my project JavaScript Demos on GitHub.

From a security perspective, randomness matters. I'm not a security expert, but as far as I can tell, a pseudo-random number generator (PRNG) is considered “secure” if it produces (or has produced) a sequence of numbers that an attacker cannot decipher.

When it comes to “generating random colors,” as in my palette utility, the concept of “random” becomes even more fuzzy. In my case, the generated colors are only as random as they appear “random” to the user. In other words, the effectiveness of randomness is a user experience (UX) feature.

So I decided to try a few random ones visual elements, using both Math.random()and crypto.getRandomValues()to check, it is felt whether there is a significant difference in the performance of the two methods. In each trial, we will have a randomly generated element and a set of randomly generated integers. Then I will use my (deeply flawed) human intuition to evaluate which attempt was better.

Method Math.random() returns a decimal fraction in the range 0 (inclusive) to 1 (exclusive). It can be used to generate random numbers by taking the result of generating a random number and multiplying it by the range of possible values.

In other words, if the method Math.random() returns 0.25, then you will choose the value that is closest to 25% in the given range from min to max. If Math.random() returned 0.97, then you would choose the value that is closest to 97% in the given range from minimum to maximum.

Method crypto.getRandomValues() works completely differently. It doesn't return a single value, instead you pass it typed array pre-selected size (length). Then the method .getRandomValues() Fills this array with random numbers generated in the range from the minimum to the maximum value. This takes into account how many values ​​can be stored in any given type.

To simplify this study, I wanted both approaches to behave roughly the same. So, rather than having to deal with decimals in one algorithm and integers in the other, I'll force both algorithms to generate decimals. That way, I'll force the value valuereturned .getRandomValues()to a decimal fraction (0..1):

value / ( maxValue + 1 )

I encapsulate this difference into two methods: randFloatWithMath() And randFloatWithCrypto():

/**
* Я возвращаю случайное число с плавающей точкой в диапазоне между 0 (включая) и 1 (исключая) при помощи модуля Math.
*/
function randFloatWithMath() {

	return Math.random();

}

/**
* Я возвращаю случайное число с плавающей точкой в диапазоне между 0 (включая) и 1 (исключая) при помощи модуля Crypto.
*/
function randFloatWithCrypto() {

	var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
	var maxInt = 4294967295;

	return ( randomInt / ( maxInt + 1 ) );

}

Having these two methods, I can then assign a reference to one of them randFloat()which can then be used to generate random values ​​in a given range by alternately using both algorithms:

/**
* Я генерирую случайное целое число в заданном диапазоне (от минимума до максимума) включительно.
*/
function randRange( min, max ) {

	return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );

}

Now let's do an experiment. Our program's user interface is simple, it works on the basis of Alpine.jsEach experiment uses the same Alpine.js component; but its constructor receives an argument that specifies which implementation randFloat() will be used:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<link rel="stylesheet" type="text/css" href="https://habr.com/ru/articles/828032/./main.css" />
</head>
<body>

	<h1>
		Exploring Randomness In JavaScript
	</h1>

	<div class="side-by-side">
		<section x-data="Explore( 'math' )">
			<h2>
				Math Module
			</h2>

			<!-- Очень большое число со случайными координатами {X,Y} -->
			<canvas
				x-ref="canvas"
				width="320"
				height="320">
			</canvas>

			<!-- Очень малое число со случайными координатами. -->
			<p x-ref="list"></p>

			<p>
				Duration: <span x-text="duration"></span>
			</p>
		</section>

		<section x-data="Explore( 'crypto' )">
			<h2>
				Crypto Module
			</h2>

			<!-- Очень большое число со случайными координатами {X,Y}. -->
			<canvas
				x-ref="canvas"
				width="320"
				height="320">
			</canvas>

			<!-- Очень малое число со случайными координатами. -->
			<p x-ref="list"></p>

			<p>
				Duration: <span x-text="duration"></span>ms
			</p>
		</section>
	</div>

	<script type="text/javascript" src="https://habr.com/ru/articles/828032/./main.js" defer></script>
	<script type="text/javascript" src="https://habr.com/ru/vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>

</body>
</html>

As you can see, each of the components x-data="Explore" contains two links x-ref: canvas And list. When initialized, the component fills these two links with generated random values, using the methods fillCanvas() And fillList().

Here is my JavaScript/Alpine.js component:

/**
* Я возвращаю случайное число с плавающей точкой в диапазоне между 0 (включая) и 1 (исключая) при помощи модуля Math.
*/
function randFloatWithMath() {

	return Math.random();

}

/**
* Я возвращаю случайное число с плавающей точкой в диапазоне между 0 (включая) и 1 (исключая) при помощи модуля Crypto.
*/
function randFloatWithCrypto() {

	// Этот метод заполняет конкретный массив случайными значениями заданного типа.
	// В нашем случае требуется всего одно случайное значение, поэтому мы передадим массив 
	// длиной 1.
	// --
	// Замечание: для повышения производительности можно было бы кэшировать типизированный массив и просто раз за разом передавать 
	// одну и ту же ссылку (получается вдвое быстрее). Но здесь мы исследуем 
	// фактор случайности, а не производительность.
	var [ randomInt ] = crypto.getRandomValues( new Uint32Array( 1 ) );
	var maxInt = 4294967295;

	// В отличие от Math.random(), метод crypto даёт нам целое число. Чтобы подставить это число обратно в 
	// математическое уравнение того же рода, что и ранее, нам придётся преобразовать целое число в десятичную дробь, так, чтобы можно было выяснить,
	// в какую именно часть выбранного диапазона нас заведёт наш фактор случайности.
	return ( randomInt / ( maxInt + 1 ) );

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

function Explore( algorithm ) {

	// Каждому экземпляру данного компонента Alpine.js присваивается отдельная стратегия рандомизации, применяемая к 
	// числам с плавающей точкой (0..1). В остальном все экземпляры компонента работают
	// совершенно одинаково.
	var randFloat = ( algorithm === "math" )
		? randFloatWithMath
		: randFloatWithCrypto
	;

	return {
		duration: 0,
		// Публичные методы
		init: init,
		// Приватные методы
		fillCanvas: fillCanvas,
		fillList: fillList,
		randRange: randRange
	}

	// ---
	// ПУБЛИЧНЫЕ МЕТОДЫ
	// ---

	/**
	* Я инициализирую компонент Alpine.js 
	*/
	function init() {

		var startedAt = Date.now();

		this.fillCanvas();
		this.fillList();

		this.duration = ( Date.now() - startedAt );

	}

	// ---
	// ПРИВАТНЫЕ МЕТОДЫ
	// ---

	/**
	* Я заполняю холст случайными пикселями с координатами {X,Y}.
	*/
	function fillCanvas() {

		var pixelCount = 200000;
		var canvas = this.$refs.canvas;
		var width = canvas.width;
		var height = canvas.height;

		var context = canvas.getContext( "2d" );
		context.fillStyle = "deeppink";

		for ( var i = 0 ; i < pixelCount ; i++ ) {

			var x = this.randRange( 0, width );
			var y = this.randRange( 0, height );

			// По мере того, как будем добавлять новые пиксели, давайте будем делать цвета пикселей всё более матовыми
			// Я надеялся, что это, возможно, поможет показать потенциальную кластеризацию значений.
			context.globalAlpha = ( i / pixelCount );
			context.fillRect( x, y, 1, 1 );

		}

	}

	/**
	* Я заполняю список случайными значениями от 0 до 9 
	*/
	function fillList() {

		var list = this.$refs.list;
		var valueCount = 105;
		var values = [];

		for ( var i = 0 ; i < valueCount ; i++ ) {

			values.push( this.randRange( 0, 9 ) );

		}

		list.textContent = values.join( " " );

	}

	/**
	* Я генерирую случайное целое число в диапазоне между заданными значениями минимума и максимума, включительно.
	*/
	function randRange( min, max ) {

		return ( min + Math.floor( randFloat() * ( max - min + 1 ) ) );

	}

}

After running this demo example, we get the following output:

As I said above, chance is, from a human point of view, a very shaky phenomenon. It is more related to sensationsthan with the mathematical concept of probability. For example, getting two identical values ​​in a row can be with the same probability as any two values ​​generated in a row. But to a person Seemsthat the same values ​​stand out somehow, as if they were special.

At the same time, comparing the two visualizations above side by side, we see that they are not particularly different from each other in terms of distribution. Of course, the module Crypto works significantly slower (half of the costs go to allocating a typed array). But it is difficult to say “by feel” which option is better.

So I came to the conclusion that Generate a color palette with random shades I probably didn't need to use the module Crypto – most likely, it was necessary to adhere to Math. It works much faster and perceived as if it gives truly random values. Material on Crypto I'll leave it to those who have to provide cryptography on the client side (I've never had to do anything like that).

Want to grab the code from this post? Read the license.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *