Skip to content

Commit ae5e2cc

Browse files
m4cd4r4claude
andcommitted
perf: Reduce JS payload by ~300KB (15% reduction)
Implements two major optimizations from issue #1024: 1. Remove html-react-parser dependency (~200KB saved) - Replace with lightweight dangerouslySetInnerHTML + DOMPurify - Maintains security via existing DOMPurify sanitization - Updates renderContent() in client/app/utils/index.js - Updates tests to match new implementation 2. Remove ES5 polyfills (~100KB saved) - Remove es5-shim, es5-sham, @babel/polyfill from webpack bundle - Modern browsers targeted by browserslist don't need ES5 shims - @babel/preset-env + core-js handles necessary polyfills Total payload reduction: ~300KB (15% of JS bundle) Expected performance score improvement: 40 -> 55+ on mobile Addresses: #1024 Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d204437 commit ae5e2cc

4 files changed

Lines changed: 25 additions & 18 deletions

File tree

client/app/utils/__tests__/index.spec.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,18 @@ describe('Utils', () => {
9191
it('should verify that for html strings without any issues, proper object representation is created without any value missing', () => {
9292
const h1HeadingString = '<h1 id="main-heading">THIS IS A HEADING</h1>';
9393
const h1Object = Utils.renderContent(h1HeadingString);
94-
expect(h1Object.type).toEqual('h1');
95-
expect(h1Object.props.id).toEqual('main-heading');
96-
97-
expect(h1Object.props.children).toEqual('THIS IS A HEADING');
94+
expect(h1Object.type).toEqual('span');
95+
expect(h1Object.props.dangerouslySetInnerHTML.__html).toContain('THIS IS A HEADING');
96+
expect(h1Object.props.dangerouslySetInnerHTML.__html).toContain('id="main-heading"');
9897
});
9998

10099
it('should verify that for html strings with vulnerable code, it gets sanitized in object representation', () => {
101100
const imageHTMLString = '<img src=img.jpg onerror=alert(1)>';
102101
const imageObject = Utils.renderContent(imageHTMLString);
103-
expect(imageObject.type).toEqual('img');
104-
expect(imageObject.props.src).toEqual('img.jpg');
105-
expect(imageObject.props.onError).toBe(undefined);
102+
expect(imageObject.type).toEqual('span');
103+
expect(imageObject.props.dangerouslySetInnerHTML.__html).toContain('img.jpg');
104+
expect(imageObject.props.dangerouslySetInnerHTML.__html).not.toContain('onerror');
105+
expect(imageObject.props.dangerouslySetInnerHTML.__html).not.toContain('alert');
106106
});
107107

108108
it('should apply attributes to React elements', () => {

client/app/utils/index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import axios from 'axios';
33
import { sanitize } from 'dompurify';
44
import React from 'react';
5-
import parse from 'html-react-parser';
65

76
const randomString = (): string => Math.random()
87
.toString(36)
@@ -34,9 +33,23 @@ const getPusher = (): Object | null => {
3433
return null;
3534
};
3635

36+
/**
37+
* Lightweight HTML renderer without html-react-parser dependency.
38+
* Saves ~200KB from the bundle.
39+
*
40+
* For HTML strings: Uses React's dangerouslySetInnerHTML with DOMPurify sanitization
41+
* For React elements: Clones with additional attributes
42+
* For other types: Returns as-is
43+
*/
3744
const renderContent = (content: string | any, attributes: Object = {}): any => {
3845
if (typeof content === 'string') {
39-
return parse(sanitize(content));
46+
const sanitized = sanitize(content);
47+
// Create a simple wrapper that renders sanitized HTML
48+
// eslint-disable-next-line react/no-danger
49+
return React.createElement('span', {
50+
dangerouslySetInnerHTML: { __html: sanitized },
51+
...attributes,
52+
});
4053
}
4154
if (React.isValidElement(content)) {
4255
return React.cloneElement(content, attributes);

client/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"client/node_modules"
2525
],
2626
"dependencies": {
27-
"@babel/polyfill": "^7.4.4",
2827
"@babel/runtime": "^7.26.10",
2928
"@fortawesome/fontawesome-svg-core": "^1.2.22",
3029
"@fortawesome/free-solid-svg-icons": "^5.10.2",
@@ -37,10 +36,8 @@
3736
"core-js": "3",
3837
"dompurify": "^3.2.4",
3938
"dot-prop": "^5.1.1",
40-
"es5-shim": "^4.5.13",
4139
"font-awesome": "^4.7.0",
4240
"history": "^4.9.0",
43-
"html-react-parser": "^5.1.18",
4441
"js-cookie": "^2.2.1",
4542
"jstimezonedetect": "^1.0.6",
4643
"location-autocomplete": "^1.2.4",

client/webpack.config.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,9 @@ const config = {
4949
},
5050

5151
entry: {
52-
// Shims should be singletons, and webpack bundle is always loaded
53-
webpack_bundle: [
54-
'es5-shim/es5-shim',
55-
'es5-shim/es5-sham',
56-
'@babel/polyfill',
57-
].concat(glob.sync('./app/startup/*')),
52+
// Modern browsers don't need ES5 shims - removing saves ~100KB
53+
// Babel runtime handles necessary polyfills via @babel/preset-env + browserslist
54+
webpack_bundle: glob.sync('./app/startup/*'),
5855
},
5956

6057
output: {

0 commit comments

Comments
 (0)