A minimal boilerplate for building frontends using Preact, TypeScript and Web Assembly.
PTW focuses on providing type-safety (TypeScript), light-weight rendering (Preact) and simple to use Web Assembly integration and bundling (using a custom webpack loader, check ./wasm.js
) while providing ability to develop a modular multi-page web application.
- Fully functional TypeScript and PReact support.
- Asynchronous-Streaming Web Assembly Loading, Compilation and Instantiation.
- Load and bundle CSS, SASS, JSON, JPEG, PNG, GIF, SVG, MP3, MP4 assets.
- Development Server with hot reloading upon changes.
- Automatic types generation for CSS classes.
- Clone the repository:
git clone git@github.com:Narasimha1997/ptw.git
- Install dependencies:
cd ptw && npm i
-
Modify
package.json
andsrc/
: In this repository, we have provided a simple example frontend application, which can be modified as your needs. -
Build or Run: To run the dev server:
npm run start #from the project root
This will start the live webpack development server.
To generate the minified build:
npm run build
This will create the build at dist/
folder.
For loading Web Assembly, we have built a custom webpack loader (See wasm.js
), the loader copies all the .wasm
files to dist/assets
and generates a wrapper code that loads web-assembly using streaming technique, in traditional web assembly compilation, the entire wasm
binary is loaded into a memory buffer and then compiled, which results in memory overhead, so this method is not practically feseable for loading large web assembly binaries on devices with low memory. The traditional approach of loading wasm files in web frontend is to bundle it as a buffer in a minified JavaScript file, this results in the large file, if this file is not chunked then on low bandwidth networks the file download may take long time and this might block the main thread from rendering the UI. The fetch
approach used in this loader will generate a fetch
call while wrapping which will get executed asynchronously by the browser while it can still load, render and hydrate the UI at the same time.
The loader provides following options:
const defaults = {
targetPath: 'assets',
targetName: '[contenthash].wasm',
compileOnly: false,
esModule: true,
}
targetPath
: The target path to install the wasm file at. The final path will be:dist/<targetPath>
, default path isdist/assets
since the default value oftargetPath
isassets
.targetName
: This is the filename of target wasm file, the final path will bedist/<targetPath>/<targetName>
. You can use any webpack generated formatting strings in this place.compileOnly
: If true, it will tell the loader to wrap the wasm file with code that just streaming compiles the wasm file. Returns a wasm module which can be instantiated with your own parameters and methods. If it is false, the generated wrapper code produces an instance with default parameters.esModule
: If true, the module will be exported as perES6
standards (i.eexport default {module}
). If false, it uses traditional export. (i.emodule.exports = {module}
).
We can just modify the loader options in webpack.config.js
file. Here is an example:
rules: {
.....
{
test: /\.wasm$/,
use: [
{
loader: path.resolve('./wasm.js'),
options: {
targetPath: 'assets',
targetName: '[name].[ext]'
}
}
]
}
....
}
The wasm files can be imported as follows:
- When
compileOnly: false
(i.e instance is produced directly)
import factLoader from './factorial.wasm';
....
// in the place you want to use:
const factInstance = await factLoader();
// assume that there is a function exported called `_Z4facti(int)`, then:
const result = factInstance._Z4facti(10);
console.log(result);
- When
compileOnly: true
(i.e a compiled module is produced)
import factLoader from './factorial.wasm';
....
// in the place you want to use:
const factModule = await factLoader();
const factInstance = await WebAssembly.instantiate(factModule);
// now you can access the exported functions:
const result = factInstance.exports._Z4facti(10);
console.log(result);
You can also use then()-catch()
style:
....
factLoader().then((modOrInst) => {
....
}).catch((err) => {
....
});
The boilerplate is designed to handle multiple web pages within the same site. The src
is organized like this (just an example):
src/
- index/
- index.tsx
- some_file.tsx
- some_file_2.tsx
- banner.jpg
- style_1.css
- style_2.css
- fib.wasm
- ....
- about/
- index.tsx
- some_file.tsx
.....
- utils/ // this is a non-web module
- config.json
- some_util_file.ts
- some_other_util_file.ts
....
In the example above, index/
and about/
are web-modules. i.e they have an index.tsx
file. They are compiled and bundled as a separate JavaScript file. The utils/
is a non-web module, it can be commonly used by any other module, they dont produce any bundle seperately. We can organize the web/
folder accordingly:
web/
- index.html
- about.html
After build, the generated folder layout in dist will be:
dist/
- js/
- index.js
- about.js
- assets/
- banner.jpg
- index.css
- fib.wasm
- index.html
- about.html
You can look at web/
for html examples. Here is sample web/index.html
page:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Hello World</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="stylesheet" href="assets/index.css">
</head>
<body>
<div id="root">
</div>
<script src='js/index.js'></script>
</body>
</html>
We have to make sure the script src is pointing to js/index.js
and stylesheet to assets/index.css
.
The boilerplate can generate load and bundle css files as well. The css file is generated one per web module. In the above example, style_1.css
and style_2.css
are bundled together in index.css
file.
factorial.wasm
binary file was taken from here.- wasm-loader - Acted as a tutorial for building my own plugin.
- webpack - Core of this project.
- Preact - The light weight alternative to React.
- TypeScript - JavaScript with compile-time type safety.
- file-loader - Function
normalizePath
was taken from this repo.
If you wish to contribute, feel free to raise any issues, make pull requests or suggest changes.