Skip to content

Commit 86115d7

Browse files
DaniilSokolyukdustinsoftware
authored andcommitted
Avoid large objects allocations and reuse everthing (#532)
* Text writer implementation * Remove redundant verifiable calls
1 parent 9234c06 commit 86115d7

File tree

8 files changed

+199
-66
lines changed

8 files changed

+199
-66
lines changed

src/React.AspNet/ActionHtmlString.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.IO;
1212

1313
#if LEGACYASPNET
14+
using System.Text;
1415
using System.Web;
1516
#else
1617
using System.Text.Encodings.Web;
@@ -40,13 +41,26 @@ public ActionHtmlString(Action<TextWriter> textWriter)
4041
}
4142

4243
#if LEGACYASPNET
44+
[ThreadStatic]
45+
private static StringWriter _sharedStringWriter;
46+
4347
/// <summary>Returns an HTML-encoded string.</summary>
4448
/// <returns>An HTML-encoded string.</returns>
4549
public string ToHtmlString()
4650
{
47-
var sw = new StringWriter();
48-
_textWriter(sw);
49-
return sw.ToString();
51+
var stringWriter = _sharedStringWriter;
52+
if (stringWriter != null)
53+
{
54+
stringWriter.GetStringBuilder().Clear();
55+
}
56+
else
57+
{
58+
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(128));
59+
}
60+
61+
_textWriter(stringWriter);
62+
63+
return stringWriter.ToString();
5064
}
5165
#else
5266
/// <summary>

src/React.AspNet/HtmlHelperExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public static IHtmlString React<T>(
8181
reactComponent.ContainerClass = containerClass;
8282
}
8383

84-
writer.Write(reactComponent.RenderHtml(clientOnly, serverOnly, exceptionHandler));
84+
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler);
8585
}
8686
finally
8787
{
@@ -131,9 +131,9 @@ public static IHtmlString ReactWithInit<T>(
131131
reactComponent.ContainerClass = containerClass;
132132
}
133133

134-
writer.Write(reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler));
134+
reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
135135
writer.WriteLine();
136-
WriteScriptTag(writer, bodyWriter => bodyWriter.Write(reactComponent.RenderJavaScript()));
136+
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
137137
}
138138
finally
139139
{
@@ -153,7 +153,7 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
153153
{
154154
try
155155
{
156-
WriteScriptTag(writer, bodyWriter => bodyWriter.Write(Environment.GetInitJavaScript(clientOnly)));
156+
WriteScriptTag(writer, bodyWriter => Environment.GetInitJavaScript(bodyWriter, clientOnly));
157157
}
158158
finally
159159
{

src/React.Core/IReactComponent.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
using System;
11+
using System.IO;
1112

1213
namespace React
1314
{
@@ -63,5 +64,24 @@ public interface IReactComponent
6364
/// </summary>
6465
/// <returns>JavaScript</returns>
6566
string RenderJavaScript();
67+
68+
/// <summary>
69+
/// Renders the HTML for this component. This will execute the component server-side and
70+
/// return the rendered HTML.
71+
/// </summary>
72+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
73+
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
74+
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
75+
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
76+
/// <returns>HTML</returns>
77+
void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null);
78+
79+
/// <summary>
80+
/// Renders the JavaScript required to initialise this component client-side. This will
81+
/// initialise the React component, which includes attach event handlers to the
82+
/// server-rendered HTML.
83+
/// </summary>
84+
/// <returns>JavaScript</returns>
85+
void RenderJavaScript(TextWriter writer);
6686
}
6787
}

src/React.Core/IReactEnvironment.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010

11+
using System.IO;
12+
1113
namespace React
1214
{
1315
/// <summary>
@@ -114,5 +116,14 @@ public interface IReactEnvironment
114116
/// Gets the site-wide configuration.
115117
/// </summary>
116118
IReactSiteConfiguration Configuration { get; }
119+
120+
/// <summary>
121+
/// Renders the JavaScript required to initialise all components client-side. This will
122+
/// attach event handlers to the server-rendered HTML.
123+
/// </summary>
124+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
125+
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
126+
/// <returns>JavaScript for all components</returns>
127+
void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
117128
}
118129
}

src/React.Core/ReactComponent.cs

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
using System;
1111
using System.Collections.Concurrent;
12+
using System.IO;
1213
using System.Linq;
14+
using System.Text;
1315
using System.Text.RegularExpressions;
1416
using JavaScriptEngineSwitcher.Core;
1517
using Newtonsoft.Json;
@@ -24,6 +26,9 @@ public class ReactComponent : IReactComponent
2426
{
2527
private static readonly ConcurrentDictionary<string, bool> _componentNameValidCache = new ConcurrentDictionary<string, bool>(StringComparer.Ordinal);
2628

29+
[ThreadStatic]
30+
private static StringWriter _sharedStringWriter;
31+
2732
/// <summary>
2833
/// Regular expression used to validate JavaScript identifiers. Used to ensure component
2934
/// names are valid.
@@ -87,8 +92,7 @@ public object Props
8792
_props = value;
8893
_serializedProps = JsonConvert.SerializeObject(
8994
value,
90-
_configuration.JsonSerializerSettings
91-
);
95+
_configuration.JsonSerializerSettings);
9296
}
9397
}
9498

@@ -119,6 +123,24 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con
119123
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
120124
/// <returns>HTML</returns>
121125
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
126+
{
127+
using (var writer = new StringWriter())
128+
{
129+
RenderHtml(writer, renderContainerOnly, renderServerOnly, exceptionHandler);
130+
return writer.ToString();
131+
}
132+
}
133+
134+
/// <summary>
135+
/// Renders the HTML for this component. This will execute the component server-side and
136+
/// return the rendered HTML.
137+
/// </summary>
138+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
139+
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
140+
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
141+
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
142+
/// <returns>HTML</returns>
143+
public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
122144
{
123145
if (!_configuration.UseServerSideRendering)
124146
{
@@ -133,16 +155,28 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
133155
var html = string.Empty;
134156
if (!renderContainerOnly)
135157
{
158+
var stringWriter = _sharedStringWriter;
159+
if (stringWriter != null)
160+
{
161+
stringWriter.GetStringBuilder().Clear();
162+
}
163+
else
164+
{
165+
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(_serializedProps.Length + 128));
166+
}
167+
136168
try
137169
{
138-
var reactRenderCommand = renderServerOnly
139-
? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
140-
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
141-
html = _environment.Execute<string>(reactRenderCommand);
170+
stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
171+
WriteComponentInitialiser(stringWriter);
172+
stringWriter.Write(')');
173+
174+
html = _environment.Execute<string>(stringWriter.ToString());
142175

143176
if (renderServerOnly)
144177
{
145-
return html;
178+
writer.Write(html);
179+
return;
146180
}
147181
}
148182
catch (JsRuntimeException ex)
@@ -156,18 +190,23 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
156190
}
157191
}
158192

159-
string attributes = string.Format("id=\"{0}\"", ContainerId);
193+
writer.Write('<');
194+
writer.Write(ContainerTag);
195+
writer.Write(" id=\"");
196+
writer.Write(ContainerId);
197+
writer.Write('"');
160198
if (!string.IsNullOrEmpty(ContainerClass))
161199
{
162-
attributes += string.Format(" class=\"{0}\"", ContainerClass);
200+
writer.Write(" class=\"");
201+
writer.Write(ContainerClass);
202+
writer.Write('"');
163203
}
164204

165-
return string.Format(
166-
"<{2} {0}>{1}</{2}>",
167-
attributes,
168-
html,
169-
ContainerTag
170-
);
205+
writer.Write('>');
206+
writer.Write(html);
207+
writer.Write("</");
208+
writer.Write(ContainerTag);
209+
writer.Write('>');
171210
}
172211

173212
/// <summary>
@@ -178,11 +217,27 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
178217
/// <returns>JavaScript</returns>
179218
public virtual string RenderJavaScript()
180219
{
181-
return string.Format(
182-
"ReactDOM.hydrate({0}, document.getElementById({1}))",
183-
GetComponentInitialiser(),
184-
JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings
185-
);
220+
using (var writer = new StringWriter())
221+
{
222+
RenderJavaScript(writer);
223+
return writer.ToString();
224+
}
225+
}
226+
227+
/// <summary>
228+
/// Renders the JavaScript required to initialise this component client-side. This will
229+
/// initialise the React component, which includes attach event handlers to the
230+
/// server-rendered HTML.
231+
/// </summary>
232+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
233+
/// <returns>JavaScript</returns>
234+
public virtual void RenderJavaScript(TextWriter writer)
235+
{
236+
writer.Write("ReactDOM.hydrate(");
237+
WriteComponentInitialiser(writer);
238+
writer.Write(", document.getElementById(\"");
239+
writer.Write(ContainerId);
240+
writer.Write("\"))");
186241
}
187242

188243
/// <summary>
@@ -208,14 +263,14 @@ protected virtual void EnsureComponentExists()
208263
/// <summary>
209264
/// Gets the JavaScript code to initialise the component
210265
/// </summary>
211-
/// <returns>JavaScript for component initialisation</returns>
212-
protected virtual string GetComponentInitialiser()
266+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
267+
protected virtual void WriteComponentInitialiser(TextWriter writer)
213268
{
214-
return string.Format(
215-
"React.createElement({0}, {1})",
216-
ComponentName,
217-
_serializedProps
218-
);
269+
writer.Write("React.createElement(");
270+
writer.Write(ComponentName);
271+
writer.Write(", ");
272+
writer.Write(_serializedProps);
273+
writer.Write(')');
219274
}
220275

221276
/// <summary>

src/React.Core/ReactEnvironment.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System;
1111
using System.Collections.Generic;
1212
using System.Diagnostics;
13+
using System.IO;
1314
using System.Reflection;
1415
using System.Text;
1516
using System.Threading;
@@ -335,25 +336,37 @@ public virtual IReactComponent CreateComponent(IReactComponent component, bool c
335336
/// <returns>JavaScript for all components</returns>
336337
public virtual string GetInitJavaScript(bool clientOnly = false)
337338
{
338-
var fullScript = new StringBuilder();
339-
339+
using (var writer = new StringWriter())
340+
{
341+
GetInitJavaScript(writer, clientOnly);
342+
return writer.ToString();
343+
}
344+
}
345+
346+
/// <summary>
347+
/// Renders the JavaScript required to initialise all components client-side. This will
348+
/// attach event handlers to the server-rendered HTML.
349+
/// </summary>
350+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
351+
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
352+
/// <returns>JavaScript for all components</returns>
353+
public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false)
354+
{
340355
// Propagate any server-side console.log calls to corresponding client-side calls.
341356
if (!clientOnly)
342357
{
343358
var consoleCalls = Execute<string>("console.getCalls()");
344-
fullScript.Append(consoleCalls);
359+
writer.Write(consoleCalls);
345360
}
346-
361+
347362
foreach (var component in _components)
348363
{
349364
if (!component.ServerOnly)
350365
{
351-
fullScript.Append(component.RenderJavaScript());
352-
fullScript.AppendLine(";");
366+
component.RenderJavaScript(writer);
367+
writer.WriteLine(';');
353368
}
354369
}
355-
356-
return fullScript.ToString();
357370
}
358371

359372
/// <summary>

0 commit comments

Comments
 (0)