Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid large objects allocations and reuse everthing #532

Merged
merged 2 commits into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/React.AspNet/ActionHtmlString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.IO;

#if LEGACYASPNET
using System.Text;
using System.Web;
#else
using System.Text.Encodings.Web;
Expand Down Expand Up @@ -40,13 +41,26 @@ public ActionHtmlString(Action<TextWriter> textWriter)
}

#if LEGACYASPNET
[ThreadStatic]
private static StringWriter _sharedStringWriter;

/// <summary>Returns an HTML-encoded string.</summary>
/// <returns>An HTML-encoded string.</returns>
public string ToHtmlString()
{
var sw = new StringWriter();
_textWriter(sw);
return sw.ToString();
var stringWriter = _sharedStringWriter;
if (stringWriter != null)
{
stringWriter.GetStringBuilder().Clear();
}
else
{
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(128));
}

_textWriter(stringWriter);

return stringWriter.ToString();
}
#else
/// <summary>
Expand Down
8 changes: 4 additions & 4 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static IHtmlString React<T>(
reactComponent.ContainerClass = containerClass;
}

writer.Write(reactComponent.RenderHtml(clientOnly, serverOnly, exceptionHandler));
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler);
}
finally
{
Expand Down Expand Up @@ -131,9 +131,9 @@ public static IHtmlString ReactWithInit<T>(
reactComponent.ContainerClass = containerClass;
}

writer.Write(reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler));
reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
writer.WriteLine();
WriteScriptTag(writer, bodyWriter => bodyWriter.Write(reactComponent.RenderJavaScript()));
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
}
finally
{
Expand All @@ -153,7 +153,7 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
{
try
{
WriteScriptTag(writer, bodyWriter => bodyWriter.Write(Environment.GetInitJavaScript(clientOnly)));
WriteScriptTag(writer, bodyWriter => Environment.GetInitJavaScript(bodyWriter, clientOnly));
}
finally
{
Expand Down
20 changes: 20 additions & 0 deletions src/React.Core/IReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

using System;
using System.IO;

namespace React
{
Expand Down Expand Up @@ -63,5 +64,24 @@ public interface IReactComponent
/// </summary>
/// <returns>JavaScript</returns>
string RenderJavaScript();

/// <summary>
/// Renders the HTML for this component. This will execute the component server-side and
/// return the rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
/// <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>
/// <returns>HTML</returns>
void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null);

/// <summary>
/// Renders the JavaScript required to initialise this component client-side. This will
/// initialise the React component, which includes attach event handlers to the
/// server-rendered HTML.
/// </summary>
/// <returns>JavaScript</returns>
void RenderJavaScript(TextWriter writer);
}
}
11 changes: 11 additions & 0 deletions src/React.Core/IReactEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/


using System.IO;

namespace React
{
/// <summary>
Expand Down Expand Up @@ -114,5 +116,14 @@ public interface IReactEnvironment
/// Gets the site-wide configuration.
/// </summary>
IReactSiteConfiguration Configuration { get; }

/// <summary>
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <returns>JavaScript for all components</returns>
void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
}
}
109 changes: 82 additions & 27 deletions src/React.Core/ReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using JavaScriptEngineSwitcher.Core;
using Newtonsoft.Json;
Expand All @@ -24,6 +26,9 @@ public class ReactComponent : IReactComponent
{
private static readonly ConcurrentDictionary<string, bool> _componentNameValidCache = new ConcurrentDictionary<string, bool>(StringComparer.Ordinal);

[ThreadStatic]
private static StringWriter _sharedStringWriter;

/// <summary>
/// Regular expression used to validate JavaScript identifiers. Used to ensure component
/// names are valid.
Expand Down Expand Up @@ -87,8 +92,7 @@ public object Props
_props = value;
_serializedProps = JsonConvert.SerializeObject(
value,
_configuration.JsonSerializerSettings
);
_configuration.JsonSerializerSettings);
}
}

Expand Down Expand Up @@ -119,6 +123,24 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con
/// <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>
/// <returns>HTML</returns>
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
{
using (var writer = new StringWriter())
{
RenderHtml(writer, renderContainerOnly, renderServerOnly, exceptionHandler);
return writer.ToString();
}
}

/// <summary>
/// Renders the HTML for this component. This will execute the component server-side and
/// return the rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
/// <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>
/// <returns>HTML</returns>
public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
{
if (!_configuration.UseServerSideRendering)
{
Expand All @@ -133,16 +155,28 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
var html = string.Empty;
if (!renderContainerOnly)
{
var stringWriter = _sharedStringWriter;
if (stringWriter != null)
{
stringWriter.GetStringBuilder().Clear();
}
else
{
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(_serializedProps.Length + 128));
}

try
{
var reactRenderCommand = renderServerOnly
? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
html = _environment.Execute<string>(reactRenderCommand);
stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
WriteComponentInitialiser(stringWriter);
stringWriter.Write(')');

html = _environment.Execute<string>(stringWriter.ToString());

if (renderServerOnly)
{
return html;
writer.Write(html);
return;
}
}
catch (JsRuntimeException ex)
Expand All @@ -156,18 +190,23 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
}
}

string attributes = string.Format("id=\"{0}\"", ContainerId);
writer.Write('<');
writer.Write(ContainerTag);
writer.Write(" id=\"");
writer.Write(ContainerId);
writer.Write('"');
if (!string.IsNullOrEmpty(ContainerClass))
{
attributes += string.Format(" class=\"{0}\"", ContainerClass);
writer.Write(" class=\"");
writer.Write(ContainerClass);
writer.Write('"');
}

return string.Format(
"<{2} {0}>{1}</{2}>",
attributes,
html,
ContainerTag
);
writer.Write('>');
writer.Write(html);
writer.Write("</");
writer.Write(ContainerTag);
writer.Write('>');
}

/// <summary>
Expand All @@ -178,11 +217,27 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
/// <returns>JavaScript</returns>
public virtual string RenderJavaScript()
{
return string.Format(
"ReactDOM.hydrate({0}, document.getElementById({1}))",
GetComponentInitialiser(),
JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings
);
using (var writer = new StringWriter())
{
RenderJavaScript(writer);
return writer.ToString();
}
}

/// <summary>
/// Renders the JavaScript required to initialise this component client-side. This will
/// initialise the React component, which includes attach event handlers to the
/// server-rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <returns>JavaScript</returns>
public virtual void RenderJavaScript(TextWriter writer)
{
writer.Write("ReactDOM.hydrate(");
WriteComponentInitialiser(writer);
writer.Write(", document.getElementById(\"");
writer.Write(ContainerId);
writer.Write("\"))");
}

/// <summary>
Expand All @@ -208,14 +263,14 @@ protected virtual void EnsureComponentExists()
/// <summary>
/// Gets the JavaScript code to initialise the component
/// </summary>
/// <returns>JavaScript for component initialisation</returns>
protected virtual string GetComponentInitialiser()
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
protected virtual void WriteComponentInitialiser(TextWriter writer)
{
return string.Format(
"React.createElement({0}, {1})",
ComponentName,
_serializedProps
);
writer.Write("React.createElement(");
writer.Write(ComponentName);
writer.Write(", ");
writer.Write(_serializedProps);
writer.Write(')');
}

/// <summary>
Expand Down
29 changes: 21 additions & 8 deletions src/React.Core/ReactEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -335,25 +336,37 @@ public virtual IReactComponent CreateComponent(IReactComponent component, bool c
/// <returns>JavaScript for all components</returns>
public virtual string GetInitJavaScript(bool clientOnly = false)
{
var fullScript = new StringBuilder();

using (var writer = new StringWriter())
{
GetInitJavaScript(writer, clientOnly);
return writer.ToString();
}
}

/// <summary>
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <returns>JavaScript for all components</returns>
public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false)
{
// Propagate any server-side console.log calls to corresponding client-side calls.
if (!clientOnly)
{
var consoleCalls = Execute<string>("console.getCalls()");
fullScript.Append(consoleCalls);
writer.Write(consoleCalls);
}

foreach (var component in _components)
{
if (!component.ServerOnly)
{
fullScript.Append(component.RenderJavaScript());
fullScript.AppendLine(";");
component.RenderJavaScript(writer);
writer.WriteLine(';');
}
}

return fullScript.ToString();
}

/// <summary>
Expand Down
Loading