SeaJS is a compact and straightforward frontend JavaScript framework designed to build web applications with minimal overhead. It provides a simple API for DOM manipulation, state management, and signal handling. While SeaJS utilizes the Real DOM, its primary strength lies in its minimalistic approach and exceptionally small bundle size of just 209 bytes. This makes it well-suited for projects where efficiency and a lightweight footprint are crucial. SeaJS aims to deliver a balance between simplicity and functionality, catering to scenarios where performance can be optimized through concise and effective code.
The primary motivation behind Sea JS was to create a simple, efficient, and easy-to-understand framework for managing state and rendering components. I wanted something that could handle basic UI tasks without the overhead of larger frameworks like React or Vue. By focusing on core functionalities, I aimed to keep the codebase minimal and maintainable, while prioritizing the absolutely smallest achievable bundle size. Here is an updated Table of Contents for your SeaJS documentation based on the latest changes:
- Overview
- Key Features
- Installation and Setup
- Basic Usage
- Core Features
- What's New
- Codebase Overview
- Contribution
- License
- State Management: Efficiently manage and update application state using a robust store mechanism that integrates with RxJS-based signals for reactive state management.
- Minimal Bundle Size: Designed to be compact and performant. With a bundle size of just under 209 B, SeaJS is the world's smallest frontend framework!
Sea JS now comes with a new basic CLI called the create-sea-app
. You can check it out here on GitHub or npm. This is recommended for people new to web dev, people who want a quick starter app and general users. You can use it either via npx or globally install it -
You can use the CLI without installing it globally by running:
npx create-sea-app <project-name>
To install the CLI globally:
npm install -g create-sea-app
After installation, you can use the CLI to create a new project:
create-sea-app <project-name>
Replace <project-name>
with the name of your project.
If however, you want to setup a new project with a custom configuration and any other module bundlers such as Webpack, Parcel, ES Build, or something else you can follow these steps.
First, you need to set up a Node.js project. In your terminal, navigate to the folder where you want to create the project and run:
npm init -y
This will create a package.json
file in your project folder, initializing the Node.js project.
Once your project is initialized, install the framework from npm:
npm i sea-js-ui-core
This will add the framework to your project dependencies.
To bundle your application, you’ll need to set up a module bundler. We recommend using Vite for fast builds and hot reloading. You can install and configure Vite by running the following commands:
npm i vite --save-dev
Update the package.json
under the "scripts"
section. Here is how you do it if you use Vite
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
In the root of your project folder, create an index.html
file that will serve as the entry point for your application. Add the following basic HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sea JS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="src/app.js"></script>
</body>
</html>
Next, create a folder called src
in the root of your project. Inside the src
folder, create a file called app.js
where you will write your main application logic.
Here’s an example of what your app.js
file might look like:
import { createComponent } from "sea-js-ui-core";
function CounterComponent(state, setState) {
function increment() {
setState({ count: state.count + 1 });
console.log(state.count + 1);
}
function decrement() {
setState({ count: state.count - 1 });
console.log(state.count - 1);
}
window.increment = increment;
window.decrement = decrement;
return `
<div>
<h1>Welcome to SeaJS!</h1>
<h6>A lightweight frontend framework made with love.</h6>
<h2>Count: ${state.count}</h2>
<button onclick="increment()">Increment</button>
<button onclick="decrement()">Decrement</button>
</div>
`;
}
createComponent(CounterComponent, { count: 0 });
Make sure the <script>
tag in index.html
correctly links to your app.js
file. The structure provided above already does this with:
<script type="module" src="/src/app.js"></script>
Now you can run the development server using Vite. Simply run:
npm start
That's it! You now have a basic setup with your framework, ready for development.
You can create components using the createComponent
function. Here’s a basic example:
import { createComponent } from "sea-js";
function CounterComponent(state, setState) {
function increment() {
setState({ count: state.count + 1 });
console.log(state.count + 1);
}
function decrement() {
setState({ count: state.count - 1 });
console.log(state.count - 1);
}
// Expose functions to the global scope
window.increment = increment;
window.decrement = decrement;
return `
<div>
<h1>Welcome to SeaJS!</h1>
<h6>A lightweight frontend framework made with love.</h6>
<h2>Count: ${state.count}</h2>
<button onclick="increment()">Increment</button>
<button onclick="decrement()">Decrement</button>
</div>
`;
}
createComponent(CounterComponent, { count: 0 });
SeaJS now provides a RxJS based store for managing application state:
import { BehaviorSubject } from 'rxjs';
const Store = (initialState = {}) => {
const state = new BehaviorSubject(initialState);
return{
getState: () => state.getValue(),
setState: newState => state.next({...state.getValue() || {}, ...newState}),
subscribe: listener => state.subscribe(listener),
};
};
window.store = Store();
The createComponent
function initializes a component with a given initial state and renders it:
export const createComponent = (fn, init) => {
store.setState(init);
const render = () => document.getElementById('root').innerHTML = fn(store.getState(), store.setState);
render();
store.subscribe(render);
};
-
RxJS Integration for State Management: The framework now incorporates RxJS-based signals for more dynamic and reactive state management. This integration enhances the ability to handle state changes and updates with greater flexibility and responsiveness.
-
Function-Based Store Implementation: The
Store
class has been further refactored into a more streamlined and functional approach. This change simplifies state management, helped us reduce the bundle size and it improves overall performance, while still leveraging RxJS's powerful reactivity. -
createComponent
Function Refactor: ThecreateComponent
function has been refactored to integrate seamlessly with the new RxJS-basedStore
implementation. This refactor allows for automatic re-rendering upon state changes and simplifies how components are initialized and updated. The updated function ensures better synchronization between state management and UI rendering, enhancing both developer experience and application performance. It is also now simpler to facilitate even smaller bundle size. -
Bundle Size Optimization: SeaJS has achieved significant reductions in bundle size through continuous optimization. The bundle size has decreased from 1037 bytes to 288 bytes, then to 245 bytes, and finally to an impressive 235 bytes. This was further improved by switching to a function based
Store
from the class based implementation, and a more aggressive setup for Rollup and Terser leading to another whopping 11.064% reduction in bundle size making it just under 209 bytes. This progression highlights our commitment to maximizing performance and efficiency. We have reduced the bundle size by a whopping 79.85% from the original v0.0.1. -
Streamlined CLI: The new
create-sea-app
CLI tool has been introduced to simplify project setup. This CLI offers a quick and easy way to generate new SeaJS projects with a single command, streamlining the development workflow. -
Updated Documentation: The documentation has been enhanced to include detailed examples and usage instructions for the latest features. This update aims to provide clearer guidance and support for both new and existing users.
-
Bug Fixes and Performance Enhancements: Various minor bugs have been addressed, and performance optimizations have been made to ensure a smoother development experience and more efficient runtime performance.
Recently, we made significant changes to our state management approach by removing our custom implementation of signals and adopting RxJS-based signals. Here’s an overview of the reasons behind these decisions:
1. Technical Challenges with Custom Signals Implementation: Our initial implementation of signals faced several technical challenges and limitations. These included compatibility concerns, performance inconsistencies, and integration difficulties with other parts of the framework.
2. Issues with window.signals
: The original window.signals
implementation was found to be unpredictable under certain circumstances. It often resulted in more re-renders than necessary due to its inefficient handling of state updates. This behavior led to performance issues and inconsistencies in the UI, prompting us to seek a more reliable solution.
3. Use of a Store Without Dedicated window.signals
: For the past five days, we utilized a store-based approach without a dedicated window.signals
object. This interim solution was implemented to simplify state management and to ensure basic functionalities remained intact while we worked on transitioning to a more reliable system. While it addressed some immediate concerns, it still lacked the reactivity and efficiency we needed.
4. Adoption of RxJS for Enhanced Reactivity: After evaluating various options, we chose RxJS due to its mature and well-established ecosystem for reactive programming. RxJS provides a powerful set of tools for managing asynchronous data streams and handling state changes efficiently. Its extensive community support and proven track record made it a suitable replacement for our custom signals.
5. Improved Performance and Reliability: RxJS offers advanced features and optimizations that enhance overall performance and reliability. By leveraging RxJS-based signals, we can provide a more consistent and responsive experience for developers and users alike, with better control over re-renders and state management.
6. Streamlined Development and Maintenance: Adopting RxJS allows us to benefit from a widely-used library with ongoing support and updates. This shift not only simplifies our development process but also reduces the maintenance burden associated with managing and updating a custom implementation.
In summary, the switch from our custom signals to RxJS and the interim use of a store-based approach without window.signals
reflect our commitment to delivering a more reliable, efficient, and maintainable framework. These changes align with our goal of providing an optimal development experience while ensuring the robustness of SeaJS.
As part of our continuous effort to optimize the framework, we transitioned from a class-based Store
implementation to a function-based one. Alongside this change, the createComponent
function was also refactored. These updates were driven by the need for a more lightweight, efficient, and flexible state management system, as well as performance optimizations. Here’s why we made this switch:
1. Bundle Size Reduction: The switch from a class-based Store
to a functional approach led to a significant reduction in bundle size. The function-based Store
is more compact and allowed us to eliminate unnecessary code. This optimization, coupled with additional changes to make Rollup and Terser more aggressive, cut down the bundle size by an impressive 11.064%, contributing to SeaJS's ongoing goal of maximizing efficiency.
2. Simplification of State Management: The function-based Store
provides a simpler, more declarative way of handling state updates and subscriptions. This streamlined approach makes the code easier to maintain, reduces the cognitive load on developers, and enhances the clarity of state management logic.
3. Performance Optimizations: Functional components are generally more performant, as they eliminate the need for class instantiation and can be more easily optimized by JavaScript engines. The function-based Store
integrates seamlessly with RxJS and allows for more efficient handling of state changes and reactivity, further boosting performance.
4. Flexibility and Extensibility: By switching to a function-based architecture, the Store
and createComponent
functions become more flexible and easier to extend. This allows developers to customize their state management logic with less effort and ensures that the framework can adapt to different use cases without being tied to a rigid class structure.
5. Better Integration with Functional Programming Paradigms: The function-based Store
aligns with modern JavaScript development practices, which favor functional programming paradigms. This update makes it easier to integrate SeaJS with other libraries and tools that follow a similar approach, resulting in a more cohesive and flexible development experience.
6. createComponent
Refactor: The createComponent
function was refactored to complement the new function-based Store
. This update not only simplifies how components are created and rendered but also optimizes re-rendering on state changes, ensuring smoother updates and reducing unnecessary performance overhead.
In summary, these changes reflect our commitment to building a more efficient, lightweight, and maintainable framework while delivering optimal performance and modern development practices.
As part of our recent updates, several significant changes have been made to the codebase to improve the handling of signals and state management. The primary modifications focus on simplifying state management and enhancing framework efficiency through the adoption of RxJS. Below is a detailed overview of these changes:
1. Original window.signals
and Store
Class
-
Original
window.signals
: The initial implementation was designed to manage reactive signals but faced issues with unpredictability and inefficient re-renders.window.signals = { listeners: {}, subscribe(signalName, callback) { if (!this.listeners[signalName]) { this.listeners[signalName] = []; } this.listeners[signalName].push(callback); }, emit(signalName, data) { if (this.listeners[signalName]) { this.listeners[signalName].forEach(callback => callback(data)); } } };
-
Original
Store
Class: The previous implementation of theStore
class was tightly coupled withwindow.signals
, leading to performance inefficiencies.class Store { constructor(initialState = {}) { this.state = initialState; this.listeners = []; } getState() { return this.state; } setState(newState) { this.state = { ...this.state, ...newState }; this.notify(); } subscribe(listener) { this.listeners.push(listener); } notify() { this.listeners.forEach(listener => listener(this.state)); } }
2. Refactored RxJS-Based Version
To address the limitations of the original implementation, we refactored the Store
class to leverage RxJS. This class-based version utilizes BehaviorSubject
for state management, providing a more robust and efficient solution:
import { BehaviorSubject } from 'rxjs';
class Store {
constructor(initialState = {}) {
this.state = new BehaviorSubject(initialState);
}
getState() {
return this.state.getValue();
}
setState(newState) {
const currentState = this.state.getValue();
const updatedState = { ...currentState, ...newState };
this.state.next(updatedState);
}
subscribe(listener) {
return this.state.subscribe(listener);
}
}
window.store = new Store();
3. New Functional Implementation
We have since transitioned to a more streamlined functional approach for the Store
, simplifying the API and enhancing performance:
import { BehaviorSubject } from 'rxjs';
const Store = (initialState = {}) => {
const state = new BehaviorSubject(initialState);
return {
getState: () => state.getValue(),
setState: newState => state.next({ ...state.getValue() || {}, ...newState }),
subscribe: listener => state.subscribe(listener),
};
};
window.store = Store();
4. Component Creation: The createComponent
function has also been updated to work with the new Store
structure, facilitating easier reactivity and rendering:
export const createComponent = (fn, init) => {
store.setState(init);
const render = () => {
document.getElementById('root').innerHTML = fn(store.getState(), store.setState);
};
render();
store.subscribe(render);
};
5. Removal of window.signals
: The window.signals
object has been removed from the codebase. RxJS’s BehaviorSubject
now handles state updates and subscriptions, effectively addressing previous issues with the custom signals implementation.
6. Bundle Size Optimization: With additional changes made to make Rollup and Terser more aggressive, the bundle size has been further optimized, achieving a reduction of 11.064% between v0.0.7 and 0.0.8. This contributes to the overall performance enhancements of SeaJS.
7. Updated Documentation and Examples: The documentation has been revised to include details on the new RxJS-based implementation and updated examples demonstrating the use of the new Store
class.
These changes aim to improve the reliability, performance, and maintainability of SeaJS by leveraging RxJS for more advanced and efficient state management.
These changes are aimed at improving the reliability, performance, and maintainability of SeaJS by leveraging RxJS for more advanced and efficient state management.
src/framework.js
: Contains the core functionality of SeaJS, including the createComponent function and a RxJS based store.dist/
: Contains the compiled and minified versions of SeaJS. This is what gets published to npm and used in projects.rollup.config.js
: Configuration for Rollup, used to bundle and optimize the code for production..babelrc
: Babel configuration for transpiling JavaScript code.public/style.css
: Boilerplate CSSapp.js
: The boilerplate counter app used for testing.tests
: Contains all of the testing code (as of now just unit tests onframework.js
)
Feel free to contribute to the development of SeaJS by submitting issues or pull requests. For detailed guidelines, please refer to the CONTRIBUTING.md file.
This project is licensed under the MIT License. See the LICENSE file for details.