Skip to content

Commit 7553eea

Browse files
authored
Merge pull request #59 from vankeisb/feature/performance
performance improvements
2 parents 8b068f1 + 4de557f commit 7553eea

6 files changed

Lines changed: 198 additions & 37 deletions

File tree

core/src/TeaCup/Cmd.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export abstract class Cmd<Msg> {
6363
/**
6464
* A command that does nothing.
6565
*/
66-
class CmdNone<Msg> extends Cmd<Msg> {
66+
// exported for perf optimisation reasons
67+
export class CmdNone<Msg> extends Cmd<Msg> {
6768
execute(dispatch: Dispatcher<Msg>): void {
6869
// it's a noop !
6970
}

core/src/TeaCup/Memoize.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2019 Rémi Van Keisbelck
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*
24+
*/
25+
26+
27+
import {memoize} from "./Memoize";
28+
29+
interface User {
30+
readonly name: string;
31+
readonly size: number;
32+
}
33+
34+
describe('memoize function', () => {
35+
36+
let count = 0;
37+
38+
function invoke<T,R>(f: (t: T) => R, arg:T, expectedResult: R, expectedCount: number) {
39+
const r = f(arg);
40+
expect(r).toEqual(expectedResult);
41+
expect(count).toEqual(expectedCount);
42+
}
43+
44+
beforeEach(() => {
45+
count = 0;
46+
})
47+
48+
test("primitive number", () => {
49+
const f = memoize((x: number) => {
50+
count++;
51+
return x + 1;
52+
});
53+
[
54+
[0, 1, 1],
55+
[1, 2, 2],
56+
[1, 2, 2],
57+
[0, 1, 3],
58+
[0, 1, 3]
59+
].forEach(([v, er, ec]) => {
60+
invoke(f, v, er, ec);
61+
});
62+
});
63+
64+
test("object ref equality", () => {
65+
const f_: (user: User) => string = u => {
66+
count++;
67+
return u.name + " " + u.size;
68+
}
69+
const f = memoize(f_);
70+
const user: User = {
71+
name: "John",
72+
size: 48,
73+
};
74+
invoke(f, user, "John 48", 1);
75+
invoke(f, user, "John 48", 1);
76+
const user2: User = {
77+
...user,
78+
size: 12,
79+
};
80+
invoke(f, user2, "John 12", 2);
81+
invoke(f, user2, "John 12", 2);
82+
});
83+
84+
test("object with compare fn", () => {
85+
const f_: (user: User) => string = u => {
86+
count++;
87+
return u.name + " " + u.size;
88+
}
89+
const f = memoize(f_, (o1, o2) => o1.name === o2.name && o1.size === o2.size);
90+
const user: User = {
91+
name: "John",
92+
size: 48,
93+
};
94+
invoke(f, user, "John 48", 1);
95+
invoke(f, { ...user }, "John 48", 1);
96+
invoke(f, { name: "John", size: 48 }, "John 48", 1);
97+
});
98+
99+
test("array ref equals", () => {
100+
const f_: (a: number[]) => string = a => {
101+
count++;
102+
return a.join("_");
103+
}
104+
const f = memoize(f_);
105+
const a = [1, 2, 3];
106+
invoke(f, a, "1_2_3", 1);
107+
invoke(f, a, "1_2_3", 1);
108+
const a2 = [...a, 4];
109+
invoke(f, a2, "1_2_3_4", 2);
110+
invoke(f, a2, "1_2_3_4", 2);
111+
});
112+
113+
});

core/src/TeaCup/Memoize.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export type F<T,R> = (t:T) => R;
2+
3+
interface Data<T,R> {
4+
readonly arg: T;
5+
readonly res: R;
6+
}
7+
8+
export function memoize<T,R>(f: F<T,R>, compareFn?: (o1: T, o2: T) => boolean): F<T,R> {
9+
let data: Data<T,R> | undefined;
10+
11+
function invoke(t:T): R {
12+
const res = f(t);
13+
data = {
14+
arg: t,
15+
res,
16+
}
17+
return res;
18+
}
19+
20+
const compare = compareFn ?? ((o1, o2) => o1 === o2);
21+
22+
return (t:T) => {
23+
if (data) {
24+
if (compare(t, data.arg)) {
25+
return data.res;
26+
} else {
27+
return invoke(t);
28+
}
29+
} else {
30+
return invoke(t);
31+
}
32+
}
33+
}

samples/src/Samples/EventsSample.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -110,30 +110,30 @@ const windowEvents = new WindowEvents<Msg>();
110110

111111
export function subscriptions(model: Model): Sub<Msg> {
112112
return Sub.batch([
113-
documentEvents.on('click', (e: MouseEvent) => (
114-
{
115-
type: 'clicked',
116-
position: {
117-
pos: [e.x, e.y],
118-
page: [e.pageX, e.pageY],
119-
offset: [e.offsetX, e.offsetY]
120-
}
121-
} as Msg
122-
)),
123-
documentEvents.on('mousemove', (e: MouseEvent) => ({
124-
type: 'moved',
125-
position: {
126-
pos: [e.x, e.y],
127-
page: [e.pageX, e.pageY],
128-
offset: [e.offsetX, e.offsetY]
129-
}
130-
} as Msg)),
131-
windowEvents.on('scroll', (e: Event) => {
132-
return {
133-
type: 'scrolled',
134-
scroll: [window.scrollX, window.scrollY]
135-
} as Msg;
136-
})
113+
// documentEvents.on('click', (e: MouseEvent) => (
114+
// {
115+
// type: 'clicked',
116+
// position: {
117+
// pos: [e.x, e.y],
118+
// page: [e.pageX, e.pageY],
119+
// offset: [e.offsetX, e.offsetY]
120+
// }
121+
// } as Msg
122+
// )),
123+
// documentEvents.on('mousemove', (e: MouseEvent) => ({
124+
// type: 'moved',
125+
// position: {
126+
// pos: [e.x, e.y],
127+
// page: [e.pageX, e.pageY],
128+
// offset: [e.offsetX, e.offsetY]
129+
// }
130+
// } as Msg)),
131+
// windowEvents.on('scroll', (e: Event) => {
132+
// return {
133+
// type: 'scrolled',
134+
// scroll: [window.scrollX, window.scrollY]
135+
// } as Msg;
136+
// })
137137
]);
138138
}
139139

tea-cup/src/TeaCup/Memo.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,32 @@
2525

2626
import * as React from 'react';
2727

28+
function compareRefEquals<T>(o1: T, o2: T): boolean {
29+
return o1 === o2;
30+
}
31+
2832
/**
29-
* Memoize the view for passed data, and wrap the function's result
30-
* into a <Memo/> component.
33+
* Memoize the view for passed data.
3134
* @param t the data to memoize.
35+
* @param compareFn optional comparison function. Defaults to ref equality
3236
*/
33-
export function memo<T>(t: T) {
37+
export function memo<T>(t: T, compareFn?: (o1: T, o2: T) => boolean) {
38+
const equals = compareFn ?? compareRefEquals;
3439
return (f: (t: T) => React.ReactNode) => {
3540
return React.createElement(Memo, {
3641
value: t,
3742
renderer: (x: any) => {
3843
return f(x);
3944
},
45+
compareFn: equals,
4046
});
4147
};
4248
}
4349

4450
interface MemoProps {
4551
value: any;
4652
renderer: (x: any) => React.ReactNode;
53+
compareFn: (o1: any, o2: any) => boolean;
4754
}
4855

4956
class Memo<T> extends React.Component<MemoProps> {
@@ -52,6 +59,6 @@ class Memo<T> extends React.Component<MemoProps> {
5259
}
5360

5461
shouldComponentUpdate(nextProps: Readonly<MemoProps>, nextState: Readonly<{}>, nextContext: any): boolean {
55-
return this.props.value !== nextProps.value;
62+
return !this.props.compareFn(this.props.value, nextProps.value);
5663
}
5764
}

tea-cup/src/TeaCup/Program.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
*/
2525

2626
import { Component, ReactNode } from 'react';
27-
import { Dispatcher, Cmd, Sub, nextUuid } from 'tea-cup-core';
27+
import { Dispatcher, Cmd, Sub, nextUuid, CmdNone } from 'tea-cup-core';
2828
import { DevToolsEvent, DevTools } from './DevTools';
2929

3030
/**
@@ -93,18 +93,25 @@ export class Program<Model, Msg> extends Component<ProgramProps<Model, Msg>, nev
9393

9494
// perform commands in a separate timout, to
9595
// make sure that this dispatch is done
96-
setTimeout(() => {
97-
// console.log("dispatch: processing commands");
98-
// debug("performing command", updated[1]);
99-
updated[1].execute(d);
100-
// debug("<<< done");
101-
}, 0);
96+
const cmd = updated[1];
97+
if (!(cmd instanceof CmdNone)) {
98+
setTimeout(() => {
99+
// console.log("dispatch: processing commands");
100+
// debug("performing command", updated[1]);
101+
updated[1].execute(d);
102+
// debug("<<< done");
103+
}, 0);
104+
}
105+
106+
const needsUpdate = this.currentModel !== updated[0];
102107

103108
this.currentModel = updated[0];
104109
this.currentSub = newSub;
105110

106111
// trigger rendering
107-
this.forceUpdate();
112+
if (needsUpdate) {
113+
this.forceUpdate();
114+
}
108115
}
109116
}
110117

0 commit comments

Comments
 (0)