{"id":13452046,"url":"https://github.com/sokra/rawact","last_synced_at":"2025-05-15T06:07:59.345Z","repository":{"id":143846112,"uuid":"156356568","full_name":"sokra/rawact","owner":"sokra","description":"[POC] A babel plugin which compiles React.js components into native DOM instructions to eliminate the need for the react library at runtime.","archived":false,"fork":false,"pushed_at":"2019-02-25T02:09:35.000Z","size":177,"stargazers_count":2526,"open_issues_count":19,"forks_count":46,"subscribers_count":62,"default_branch":"master","last_synced_at":"2025-05-10T15:02:38.744Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sokra.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-11-06T09:11:57.000Z","updated_at":"2025-05-01T11:22:01.000Z","dependencies_parsed_at":"2023-09-23T14:35:07.553Z","dependency_job_id":null,"html_url":"https://github.com/sokra/rawact","commit_stats":{"total_commits":58,"total_committers":8,"mean_commits":7.25,"dds":"0.39655172413793105","last_synced_commit":"706291288d930b9a8c8772e37d8ed23e6ca23f56"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sokra%2Frawact","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sokra%2Frawact/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sokra%2Frawact/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sokra%2Frawact/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sokra","download_url":"https://codeload.github.com/sokra/rawact/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254262457,"owners_count":22041419,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T07:01:11.163Z","updated_at":"2025-05-15T06:07:59.319Z","avatar_url":"https://github.com/sokra.png","language":"JavaScript","readme":"# babel-plugin-rawact\n\nA babel plugin which compiles React.js components into native DOM instructions to eliminate the need for the react library at runtime.\n\n## Motivation\n\nReact.js is split into two packages (for in browser usage): react and react-dom.\n\nThe react package is a general way to describe components and elements. The react-dom package takes care of rendering these generic elements.\n\nBecause of this design react-dom includes code for every possible component/HTMLElement that can be rendered. It also includes code for incremental rendering, scheduling, event handling, etc.\n\nThis has an overhead on the initial page load for downloading and evaluating the library.\n\nBut there are applications which do not need all these features (at initial page load). For such applications it might make sense to use native DOM operations to build the interactive user interface. A prominent example is Netflix, that removed client-side React.js from the landing page and rebuild interactivity with native DOM code.\n\nThis approach is doable, but lacks DX. Writing React.js components is simpler than writing native DOM code.\n\nWhat if we could transpile React.js components to native DOM operations at build-time? This would eliminate the need for the react library at cost of a bit larger component code.\n\n[Svelte](https://svelte.technology/) has proven that this type of framework-eliminating transpilation can work very well.\n\n## Introducing Rawact\n\nRawact (raw-react) is a babel plugin which does this transformation.\n\n**State: This is in PROOF OF CONCEPT state. DO NOT USE IT IN PRODUCTION!**\n\n## When does it make sense?\n\nAssuming this tool would make it to a complete and stable tool, it could make sense to use in these cases:\n\n- Small application with total components code \u003c react size\n  - Rawact will decrease the total JS size\n- Code Splitted application with priority on initial rendering\n  - Without loading react the application will bootstrap faster\n- Compiling WebComponents from React components\n  - You don't want to ship React with your components\n- Applications with many component instances which are changing frequently\n  - Using direct DOM operations has less overhead than using a framework\n- Applications running on low-end devices\n  - Using direct DOM operations has less overhead than using a framework\n\nEvery application is different. Best measure it to see if it makes sense in your case.\n\n## Usage\n\n```\nnpm install -D babel-plugin-rawact\n```\n\n```\n// .babelrc.js\nplugins: [\n\t\"rawact\"\n]\n```\n\nMake sure that transpile all modules that contains imports to `react` or `react-dom`. This may include modules in `node_modules` when they ship React.js components.\n\n## Demo application\n\nApp: [https://sokra.github.io/todo-mvc-react-hooks-experiments/index.html](https://sokra.github.io/todo-mvc-react-hooks-experiments/index.html)\n\nRepo: [https://github.com/sokra/todo-mvc-react-hooks-experiments](https://github.com/sokra/todo-mvc-react-hooks-experiments)\n\nThis is basically the only application working for this proof of concept.\n\nwith rawact: 19.8 KiB\n\nwith react: 126 KiB\n\n### Demo 2\n\nThere is also a demo in the `app/` folder. Install dependencies in root and `app`. Run `yarn build`. Open `index.html`.\n\nThere is a performance example which is able to render 10000 non-pure elements a couple of times faster than React.js.\n\n## How does it work?\n\n1. It replaces all imports to `react` and `react-dom` with rawacts own runtime (that's much smaller).\n2. It replaces all `React.createElement` calls with DOM rendering instructions. (This includes JSX which is transpiled to `React.createElement`)\n\n### Rendering instructions\n\nrawact has the notation of rendering instructions, which are functions usually called with two arguments `context` and `rerender`. The function is expected to return a native DOM Node or DocumentFragment.\n\n`context` is an plain object which allows to store data between render calls. Similar rendering instructions may use this information in `context` to update an existing DOM Node or DocumentFragment and return this.\n\n`context._` stores a unique token to identify the rendering instruction structure. Rendering instructions use this a marker to identifier \"similar\" instructions.\n\n`context.$` may stores a function to unmount the current rendering instructions. It's expected to be called when rendering instructions are not \"similar\" and the old Node can't be used.\n\nOther properties can be used in any way by the rendering instructions.\n\n`rerender` is a function which can be called to trigger a new execution of the rendering instructions (and node update if a new node is returned).\n\n### React.createElement\n\n#### static elements\n\nInput:\n\n```jsx\nreturn \u003cdiv className=\"test\" /\u003e;\n```\n\nAnnotated output:\n\n```js\n// module scope\nconst instructions = {};\n\nreturn context =\u003e {\n\t// Check if rendering instructions are \"similar\"\n\tif (context._ !== instructions) {\n\t\t// Unmount old rendering instructions\n\t\tif (context.$) context.$();\n\t\t// Set own unmount functions and instructions marker\n\t\tcontext.$ = null;\n\t\tcontext._ = instructions;\n\n\t\t// Create DOM element\n\t\tcontext.a = createElement(\"div\");\n\n\t\t// Set properties\n\t\tcontext.a.className = \"test\";\n\t}\n\treturn context.a;\n};\n```\n\n#### Dynamic attributes\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn \u003cdiv className={test} /\u003e;\n};\n```\n\nAnnotated output:\n\n```js\nconst instructions = {};\nexport default ({ test }) =\u003e {\n\t// capture current value of attribute\n\tconst _className = test;\n\n\treturn context =\u003e {\n\t\tif (context._ !== instructions) {\n\t\t\tif (context.$) context.$();\n\t\t\tcontext.$ = null;\n\t\t\tcontext._ = instructions;\n\t\t\tcontext.a = createElement(\"div\");\n\n\t\t\t// Store old value in context.b\n\t\t\tcontext.a.className = context.b = _className;\n\t\t} else {\n\t\t\t// Check if className changed, and update it\n\t\t\tif (context.b !== _className)\n\t\t\t\tcontext.a.className = context.b = _className;\n\t\t}\n\t\treturn context.a;\n\t};\n};\n```\n\nNote: Some attributes generate different code. I. e. `onClick` generates code that adds event handlers.\n\nNote: With the compile step, the update path only has to check dynamic attributes. While React.js itself can't differ between static and dynamic attributes and has to compare all of them.\n\n#### Children\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn \u003cdiv\u003eHello {test}!\u003c/div\u003e;\n};\n```\n\nAnnotated output:\n\n```js\nconst instructions = {};\nexport default ({ test }) =\u003e {\n\t// capture current value of children\n\tconst _children_ = test;\n\n\treturn context =\u003e {\n\t\tif (context._ !== instructions) {\n\t\t\tif (context.$) context.$();\n\t\t\tcontext.$ = null;\n\t\t\tcontext._ = instructions;\n\t\t\tcontext.a = createElement(\"div\");\n\n\t\t\t// Render the child, can be text, array or rendering instructions\n\t\t\t// node is stored in context.b\n\t\t\t// child context (when rendering instructions) is stored in context.b_\n\t\t\t// old text value (for comparing) is stored in context.b$\n\t\t\trenderInternal(context, _children, \"b\", 1);\n\n\t\t\t// render all the children\n\t\t\t// shortcut for text values\n\t\t\trenderChildren(context.a, [\"Hello \", context.b, \"!\"]);\n\t\t} else {\n\t\t\t// Update the child\n\t\t\t// When node changes it's replaced in the parentElement\n\t\t\trenderInternal(context, _children, \"b\", 0);\n\t\t}\n\t\treturn context.a;\n\t};\n};\n```\n\n#### Nested elements\n\nThis is an optional optimization, as the code both could already handle nested elements.\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003ch1\u003eHello {test}!\u003c/h1\u003e\n\t\t\u003c/div\u003e\n\t);\n};\n```\n\nAnnotated output:\n\n```js\nconst instructions = {};\nexport default ({ test }) =\u003e {\n\t// capture current value of children\n\tconst _children_ = test;\n\n\treturn context =\u003e {\n\t\tif (context._ !== instructions) {\n\t\t\tif (context.$) context.$();\n\t\t\tcontext.$ = null;\n\t\t\tcontext._ = instructions;\n\t\t\tcontext.a = createElement(\"div\");\n\t\t\tcontext.b = createElement(\"h1\");\n\t\t\trenderInternal(context, _children, \"c\", 1);\n\t\t\trenderChildren(context.b, [\"Hello \", context.c, \"!\"]);\n\t\t\trenderChildren(context.a, [context.b]);\n\t\t} else {\n\t\t\trenderInternal(context, _children, \"c\", 0);\n\t\t}\n\t\treturn context.a;\n\t};\n};\n```\n\n#### Components\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn \u003cComponent prop={test}\u003eHello\u003c/Component\u003e;\n};\n```\n\nAnnotated output:\n\n```js\nconst instructions = {};\nexport default ({ test }) =\u003e {\n\treturn createComponent(Component, { prop: test, children: \"Hello\" });\n};\n```\n\n`createComponent` is a 30 lines function which returns rendering instructions to handle React.js Hooks.\n\nHere the `rerender` argument is used and triggered i. e. by `useState`. `rerender` is usually scheduled (Currently into the next microtask).\n\nThe returned rendering instructions run the `Component` (either `render` or the function itself).\n\nFor hooks a array per component and a component tree is provided.\n\n`useState` `useReducer` `useEffect` push to this array. `useContext` walks the component tree.\n\n### ReactDOM.render\n\nRendering a element is now as simple as creating a (singleton per parentNode) `context`, running the rendering instructions, appending/replacing the returned Node and running effects from `useEffect`.\n\n### React.Component\n\nA base class is provided and uses hooks to implement the behavior of instance methods.\n\n## Features and State\n\n- html elements\n- attributes (INCOMPLETE: only value, events, style and simple properties)\n- ref\n- nested elements\n- Components\n- Arrays\n  - key is not implemented\n- React.Component (PARTIAL)\n  - componentDidMount\n  - componentWillUnmount\n  - shouldComponentUpdate\n  - componentDidUpdate\n  - setState\n  - this.props\n  - this.state\n- React.Fragment\n- children\n  - React.Children is NOT IMPLEMENTED\n- useState\n- useEffect\n- useMemo\n- useRef\n- useReducer\n- React.memo\n- React.createContext\n- useContext\n- ReactDOM.render\n- ReadtDOM.unmountComponentAtNode NOT IMPLEMENTED\n- dynamic props (PARTIAL: only input with some props)\n  - code for all possible props is generated\n  - unknown props (i. e. data-xx) is not implemented\n- sync rendering\n\n## Future Work\n\n### Suspend \u0026 incremental rendering\n\nTo support this we need to change the design a bit. Running the Component function need to be separated from applying the update.\n\nIt could be implemented by rendering instructions returning a function to update the DOM update.\n\nInput:\n\n```jsx\nexport default () =\u003e {\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003cComponent /\u003e\n\t\t\t\u003cComponent /\u003e\n\t\t\u003c/div\u003e\n\t);\n};\n```\n\n```jsx\nconst instructions = {};\n\nexport default () =\u003e {\n\tconst _child1 = createComponent(Component);\n\tconst _child2 = createComponent(Component);\n\treturn context =\u003e {\n\t\tconst _childRender1 = prepareRenderInternal(context, _child1, \"b\");\n\t\tconst _childRender2 = prepareRenderInternal(context, _child2, \"c\");\n\t\treturn () =\u003e {\n\t\t\tif (context._ !== instructions) {\n\t\t\t\tif (context.$) context.$();\n\t\t\t\tcontext.$ = null;\n\t\t\t\tcontext._ = instructions;\n\t\t\t\tcontext.a = createElement(\"div\");\n\t\t\t\t_childRender1(1);\n\t\t\t\t_childRender2(1);\n\t\t\t\trenderChildren(context.a, [context.b, context.c]);\n\t\t\t} else {\n\t\t\t\t_childRender1();\n\t\t\t\t_childRender2();\n\t\t\t}\n\t\t};\n\t};\n};\n```\n\n### Merge instructions with unmount\n\nTechnically the unmount function only depend on context values. It could be hoisted to module scope and called with `context` argument. The unmount function could then be used as instruction marker, basically merging `context._` and `context.$`.\n\nThis could save a few lines of code per component and a function allocation.\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn \u003cbutton onClick={test} /\u003e;\n};\n```\n\nAnnotated output:\n\n```js\nconst unmountAndInstructions = context =\u003e {\n\tremoveEventListener(context.a, \"click\", context.b);\n};\nexport default ({ test }) =\u003e {\n\tconst _onClick = test;\n\n\treturn context =\u003e {\n\t\tif (context._ !== unmountAndInstructions) {\n\t\t\tif (context._) context._(context);\n\t\t\tcontext._ = unmountAndInstructions;\n\t\t\tcontext.a = createElement(\"button\");\n\t\t\taddEventListener(context.a, \"click\", (context.b = _onClick));\n\t\t} else {\n\t\t\tif (context.b !== _onClick)\n\t\t\t\treplaceEventListener(\n\t\t\t\t\tcontext.a,\n\t\t\t\t\t\"click\",\n\t\t\t\t\tcontext.b,\n\t\t\t\t\t(context.b = _onClick)\n\t\t\t\t);\n\t\t}\n\t\treturn context.a;\n\t};\n};\n```\n\n### Server-side rendering\n\nThis could be implemented as alternative transpiling mode. This mode would transpile for `renderToString`.\n\nTo be able to reuse the DOM created by SSR, we can create JS code on the server to recreate the `context` on the client. This would even work when there is a diff between SSR'd HTML and client-side rendering.\n\nInput:\n\n```jsx\nconst Component = () =\u003e \u003cbutton /\u003e;\n\nexport default ({ test }) =\u003e {\n\treturn (\n\t\t\u003cdiv className={test}\u003e\n\t\t\t\u003cComponent /\u003e\n\t\t\u003c/div\u003e\n\t);\n};\n```\n\nServer output:\n\n```js\nconst instructions = \"hr23s\";\nconst instructions2 = \"x7fe2\";\n\nconst Component = () =\u003e ssrContext =\u003e {\n\tssrContext.add(\"_\", JSON.stringify(instructions));\n\tssrContext.add(\"a\", ssrContext.node);\n\tssrContext.setNodeCount(1);\n\treturn `\u003cbutton\u003e`;\n};\n\nexport default ({ test }) =\u003e {\n\tconst _className = test;\n\tconst _child = ssrCreateComponent(Component);\n\n\treturn ssrContext =\u003e {\n\t\tssrContext.add(\"_\", JSON.stringify(instructions2));\n\t\tssrContext.add(\"a\", ssrContext.node);\n\t\tssrContext.add(\"b\", JSON.stringify(_className));\n\t\tconst container = ssrContext.createNodesContainer(\n\t\t\t`${ssrContext.node}.childNodes`\n\t\t);\n\t\tconst childContext = ssrContext.createChildContext(\n\t\t\tcontainer,\n\t\t\t/* node slot */ \"c\",\n\t\t\t/* context slot */ \"c_\"\n\t\t);\n\t\tconst childHtml = ssrRenderInternal(childContext, _child);\n\t\tssrContext.setNodeCount(1);\n\t\treturn `\u003cdiv class=\"${escape(_className)}\"\u003e${childHtml}\u003c/div\u003e`;\n\t};\n};\n```\n\nGenerated HTML: `\u003cdiv class=\"test-class-name\"\u003e\u003cbutton\u003e\u003c/div\u003e`\n\nContext emitting this code (`ssrContext.toCode()`):\n\n```js\nconst SSR_GENERATED_CONTEXT = a =\u003e {\n\tvar b, c;\n\treturn {\n\t\t_: \"x7fe2\",\n\t\ta: (b = a.childNodes[0]),\n\t\tb: \"test-class-name\",\n\t\tc_: {\n\t\t\t_: \"hr23s\",\n\t\t\ta: (c = b.childNodes[0])\n\t\t},\n\t\tc: c\n\t};\n};\n```\n\nClient bootstrapping:\n\n```js\nconst mountNode = document.getElementById(\"root\");\n\n// This sets the context for mountNode\nReact.restoreFromSSR(mountNode, SSR_GENERATED_CONTEXT);\n\n// This will render with the precreated context\n// =\u003e Will do a update (nop if nothing changed since than)\nReact.render(\u003cApp /\u003e, mountNode);\n```\n\n### Minimizing generated code size\n\nSome repeated code could be moved into helpers.\n\nExample: All render instruction start with the some code. This could be moved into a helper:\n\nInput:\n\n```jsx\nexport default ({ test }) =\u003e {\n\treturn \u003cdiv className={test} /\u003e;\n};\n```\n\nAnnotated output:\n\n```js\nconst instructions = {};\nexport default ({ test }) =\u003e {\n\tconst _className = test;\n\n\treturn context =\u003e {\n\t\tif (htmlElementInstructions(context, instructions, \"div\")) {\n\t\t\tcontext.a.className = context.b = _className;\n\t\t} else {\n\t\t\tif (context.b !== _className)\n\t\t\t\tcontext.a.className = context.b = _className;\n\t\t}\n\t\treturn context.a;\n\t};\n};\n```\n\nMinimized output:\n\n```js\nconst I={}\nexport default({test:a})=\u003ec=\u003e(h(c,I,\"div\")?c.a.className=c.b=a:c.b!==a\u0026\u0026c.a.className=c.b=a,c.a)\n```\n\nPretty minimized output:\n\n```js\nconst I={}\nexport default ({ test: a }) =\u003e c =\u003e (\n\th(c, I, \"div\")\n\t\t? c.a.className = c.b = a\n\t\t: c.b !== a \u0026\u0026 c.a.className = c.b = a,\n\tc.a\n)\n```\n\nFor comparison the component without rawact minimized:\n\n```js\nexport default ({ test: t }) =\u003e a(\"div\", { className: t });\n```\n\n## Size of minimal example\n\n```js\nimport React, { useState } from \"react\";\nimport ReactDOM from \"react-dom\";\n\nconst Counter = ({ step }) =\u003e {\n\tconst [counter, setCounter] = useState(0);\n\treturn \u003cbutton onClick={() =\u003e setCounter(counter + step)}\u003e{counter}\u003c/button\u003e;\n};\n\nReactDOM.render(\u003cCounter step={1} /\u003e, document.getElementById(\"root\"));\n```\n\nproduces a bundle with 4.3 KiB compared to 115 KiB with react and react-dom.\n","funding_links":[],"categories":["JavaScript","babel","前端常用"],"sub_categories":["非 JavaScript 编译工具"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsokra%2Frawact","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsokra%2Frawact","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsokra%2Frawact/lists"}