iTranslated by AI
Comparing Web Application Performance: SwiftWasm vs. JavaScript
Introduction
I started exploring and developing with SwiftWasm after talking with Yuta Saito, a developer of SwiftWasm, at the development camp linked below:
Since I had never touched SwiftWasm before, I will introduce what SwiftWasm is, its benefits, and implementation examples. I hope this article serves as a trigger for you to try out or consider adopting SwiftWasm.
SwiftWasm
First, WebAssembly (Wasm) is a binary instruction set that can be executed on web browsers at near-native speeds. SwiftWasm is an open-source project and toolchain that enables Swift to be used as a compile target for WebAssembly.
Benefits of running Swift on WebAssembly
- Reuse of existing assets: Business logic and other code written in Swift can be shared not only on iOS but also in the web frontend.
- Type-safe web development: While JavaScript is a dynamically typed language, Swift is a statically typed language, allowing for earlier discovery of errors during compilation.
- Leveraging the Swift ecosystem: iOS engineers can use the syntax, coding conventions, and tools they are familiar with to develop web frontends, reducing the learning cost.
- Speeding up computationally intensive tasks: Compared to JavaScript, SwiftWasm uses a binary format instruction set, allowing the browser to parse Wasm and compile it into executable machine code very quickly and efficiently.
Setting up Swift SDKs for WebAssembly
I referred to the following for SwiftWasm installation steps:
SwiftWasm's JavaScriptKit
In this article, I will use JavaScriptKit, a Swift framework that interfaces with JavaScript via WebAssembly, to compare the performance between JavaScript and SwiftWasm. JavaScriptKit provides a way to interact seamlessly with JavaScript when Swift code is compiled into WebAssembly.
The link below provides a tutorial on the steps to create a simple web application using JavaScriptKit:
I referred to the documentation for JavaScriptKit at the following link:
Performance comparison of grayscale calculation between SwiftWasm's JavaScriptKit and JavaScript
First, grayscale is a representation of an image using only degrees of brightness without color.
There are various methods for grayscale calculation, but here we will compare them using the luminance algorithm of the ITU-R Rec BT.601 standard as follows:
V = 0.299 * R + 0.587 * G + 0.114 * B
Grayscale calculation in JavaScript
The following is the code for grayscale calculation in JavaScript:
function jsGrayscale(data, length) {
// For storing processed data (a copy of the original array)
const processedData = Uint8ClampedArray.from(data.slice());
// Iterate over RGBA data (4 bytes per pixel)
for (let i = 0; i < length; i += 4) {
const r = processedData[i];
const g = processedData[i + 1];
const b = processedData[i + 2];
// Luminance algorithm
const grayValue = 0.299 * r + 0.587 * g + 0.114 * b;
// Replace R, G, B values with the luminance value
processedData[i] = grayValue; // R
processedData[i + 1] = grayValue; // G
processedData[i + 2] = grayValue; // B
}
return processedData;
}
Grayscale calculation in SwiftWasm with JavaScriptKit
Since the purpose of this article is to learn SwiftWasm, I will explain the code in detail based on the JavaScriptKit documentation.
Below is the code for grayscale calculation in SwiftWasm:
import JavaScriptKit
JSObject.global["convertBySwift"] = .object(
JSClosure { args in
let imageDataJS = args[0].object!
let typedArray = JSUInt8ClampedArray(unsafelyWrapping: imageDataJS)
return typedArray.withUnsafeBytes { makeGrascale(data: $0) }.jsValue
}
)
By using JSObject, you can access Swift-written logic from the global object in JavaScript. By writing JSObject.global["convertBySwift"], we set a global JavaScript function name called convertBySwift.
JSClosure represents a JavaScript function written in Swift. This type can be passed as a callback handler to a JavaScript function. This allows the convertBySwift Swift function to be called directly from JavaScript code. When called from the JavaScript side, JSClosure { args in ... } is executed, receiving the args array of [JSValue] passed from JavaScript to Swift, and returning a JSValue type to JavaScript as the result of the processing within the closure.
We retrieve the first argument passed from JavaScript as a JSObject type with let imageDataJS = args[0].object!.
We wrap the retrieved JSObject with a wrapper struct called JSUInt8ClampedArray using let typedArray = JSUInt8ClampedArray(unsafelyWrapping: imageDataJS).
Using typedArray.withUnsafeBytes, we acquire an UnsafeBufferPointer<UInt8> that allows direct and safe access to the memory area pointed to by the JSUInt8ClampedArray, and pass it to the makeGrayscale function to execute the processing. With .jsValue, we convert the result (JSUInt8ClampedArray) returned from the makeGrayscale function into a JSValue type that JavaScript can recognize, and return it to the caller in JavaScript.
Next, let's take a look at the grayscale processing in the makeGrayscale function!
func makeGrascale(data: UnsafeBufferPointer<UInt8>) -> JSUInt8ClampedArray {
let count = data.count
// Allocate a new buffer and initialize it with the original data (copy)
let out = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: count)
_ = out.initialize(from: data) // <- Executes initialization and copy at the same time
defer {
out.deallocate()
}
// Perform grayscale calculation
for i in stride(from: 0, to: count, by: 4) {
let r = Float(out[i])
let g = Float(out[i + 1])
let b = Float(out[i + 2])
// Luminance algorithm
let grayValue = UInt8(0.299 * r + 0.587 * g + 0.114 * b)
out[i] = grayValue // Overwrite R
out[i + 1] = grayValue // Overwrite G
out[i + 2] = grayValue // Overwrite B
}
// Return the result
return JSUInt8ClampedArray(buffer: UnsafeBufferPointer(out))
}
Memory allocation and data copying
Through Swift's makeGrascale(data: UnsafeBufferPointer<UInt8>), we receive a read-only raw byte data buffer (image pixel data) as an argument.
With let out = UnsafeMutableBufferPointer<UInt8>.allocate(...), because the grayscale process requires rewriting data, we allocate a writable buffer of the same size as the original data on the Swift heap memory.
_ = out.initialize(from: data) initializes the allocated out buffer with the input data content, creating a complete copy of the pixel data.
defer { out.deallocate() } is an important block for memory management. It reserves the freeing of the Swift heap memory allocated by out whenever the execution of the makeGrayscale function finishes.
Grayscale calculation process
Since grayscale data has a 4-byte structure per pixel, we use the stride(from:to:by:) function to loop through the total number of bytes in the pixel data (count) using stride(from: 0, to: count, by: 4).
As shown below, data is stored continuously in 4-byte units in the order of R, G, B, A.
out[i] gets the byte value of R (Red).
out[i + 1] gets the byte value of G (Green).
out[i + 2] gets the byte value of B (Blue).
Note: out[i + 3] is technically the alpha (transparency) component, but since it is not usually changed in grayscale conversion, it is ignored in this code.
// Perform grayscale calculation
for i in stride(from: 0, to: count, by: 4) {
let r = Float(out[i])
let g = Float(out[i + 1])
let b = Float(out[i + 2])
// Luminance algorithm
let grayValue = UInt8(0.299 * r + 0.587 * g + 0.114 * b)
out[i] = grayValue // Overwrite R
out[i + 1] = grayValue // Overwrite G
out[i + 2] = grayValue // Overwrite B
}
We convert to grayscale by overwriting the values with grayValue, which is the brightness level of the gray calculated by the luminance algorithm let grayValue = UInt8(0.299 * r + 0.587 * g + 0.114 * b).
Finally, we return the grayscale-converted JSUInt8ClampedArray(buffer: UnsafeBufferPointer(out)) as the result.
Results
When converting an 8K image to grayscale using JavaScript, the time taken was approximately 150 milliseconds, as shown in the image below.

When converting an 8K image to grayscale using SwiftWasm, the time taken was approximately 100 milliseconds, as shown in the image below.

Although it varies depending on the resolution of the image being grayscale-converted, the grayscale conversion processing using SwiftWasm was approximately 1.5 times faster for this 8K image.
Conclusion
I have uploaded the repository comparing the performance of grayscale calculation between SwiftWasm's JavaScriptKit and JavaScript to GitHub, so please take a look if you are interested.
In this instance, I only wrote the grayscale calculation logic in Swift, but by using the JavaScriptKit library, it is also possible to create UIs by manipulating the DOM with Swift code via JavaScriptKit.
References
Discussion