Skip to content

Avoid memory allocation and improve performance, part 1 #522

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

Closed
Closed
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
77 changes: 77 additions & 0 deletions src/React.AspNet/ActionHtmlString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

using System;
using System.IO;

#if LEGACYASPNET
using System.Text;
using System.Web;
#else
using System.Text.Encodings.Web;
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
#endif

#if LEGACYASPNET
namespace React.Web.Mvc
#else
namespace React.AspNet
#endif
{
/// <summary>
/// IHtmlString or IHtmlString action wrapper implementation
/// </summary>
public class ActionHtmlString : IHtmlString
{
private readonly Action<TextWriter> _textWriter;

/// <summary>
/// Constructor IHtmlString or IHtmlString action wrapper implementation
/// </summary>
/// <param name="textWriter"></param>
public ActionHtmlString(Action<TextWriter> textWriter)
{
_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 stringWriter = _sharedStringWriter;
if (stringWriter != null)
{
stringWriter.GetStringBuilder().Clear();
}
else
{
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(512));
}

_textWriter(stringWriter);
return stringWriter.ToString();
}
#else
/// <summary>
/// Writes the content by encoding it with the specified <paramref name="encoder" />
/// to the specified <paramref name="writer" />.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written.</param>
/// <param name="encoder">The <see cref="T:System.Text.Encodings.Web.HtmlEncoder" /> which encodes the content to be written.</param>
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
_textWriter(writer);
}
#endif
}
}
124 changes: 54 additions & 70 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@

#if LEGACYASPNET
using System.Web;
using System.Web.Mvc;
using IHtmlHelper = System.Web.Mvc.HtmlHelper;
#else
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
using Microsoft.AspNetCore.Html;
#endif

#if LEGACYASPNET
Expand All @@ -32,7 +29,6 @@ namespace React.AspNet
/// </summary>
public static class HtmlHelperExtensions
{

/// <summary>
/// Gets the React environment
/// </summary>
Expand Down Expand Up @@ -70,24 +66,28 @@ public static IHtmlString React<T>(
Action<Exception, string, string> exceptionHandler = null
)
{
try
return new ActionHtmlString(writer =>
{
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly);
if (!string.IsNullOrEmpty(htmlTag))
try
{
reactComponent.ContainerTag = htmlTag;
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly);
if (!string.IsNullOrEmpty(htmlTag))
{
reactComponent.ContainerTag = htmlTag;
}

if (!string.IsNullOrEmpty(containerClass))
{
reactComponent.ContainerClass = containerClass;
}

reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler);
}
if (!string.IsNullOrEmpty(containerClass))
finally
{
reactComponent.ContainerClass = containerClass;
Environment.ReturnEngineToPool();
}
var result = reactComponent.RenderHtml(clientOnly, serverOnly, exceptionHandler);
return new HtmlString(result);
}
finally
{
Environment.ReturnEngineToPool();
}
});
}

/// <summary>
Expand Down Expand Up @@ -116,25 +116,30 @@ public static IHtmlString ReactWithInit<T>(
Action<Exception, string, string> exceptionHandler = null
)
{
try
return new ActionHtmlString(writer =>
{
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly);
if (!string.IsNullOrEmpty(htmlTag))
try
{
reactComponent.ContainerTag = htmlTag;
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly);
if (!string.IsNullOrEmpty(htmlTag))
{
reactComponent.ContainerTag = htmlTag;
}

if (!string.IsNullOrEmpty(containerClass))
{
reactComponent.ContainerClass = containerClass;
}

reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
writer.WriteLine();
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
}
if (!string.IsNullOrEmpty(containerClass))
finally
{
reactComponent.ContainerClass = containerClass;
Environment.ReturnEngineToPool();
}
var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler);

return new HtmlString(html + System.Environment.NewLine + RenderToString(GetScriptTag(reactComponent.RenderJavaScript())));
}
finally
{
Environment.ReturnEngineToPool();
}
});
}

/// <summary>
Expand All @@ -144,55 +149,34 @@ public static IHtmlString ReactWithInit<T>(
/// <returns>JavaScript for all components</returns>
public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool clientOnly = false)
{
try
{
return GetScriptTag(Environment.GetInitJavaScript(clientOnly));
}
finally
return new ActionHtmlString(writer =>
{
Environment.ReturnEngineToPool();
}
try
{
WriteScriptTag(writer, bodyWriter => Environment.GetInitJavaScript(bodyWriter, clientOnly));
}
finally
{
Environment.ReturnEngineToPool();
}
});
}

private static IHtmlString GetScriptTag(string script)
private static void WriteScriptTag(TextWriter writer, Action<TextWriter> bodyWriter)
{
#if LEGACYASPNET
var tag = new TagBuilder("script")
{
InnerHtml = script,
};

writer.Write("<script");
if (Environment.Configuration.ScriptNonceProvider != null)
{
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
writer.Write(" nonce=\"");
writer.Write(Environment.Configuration.ScriptNonceProvider());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we're manually constructing HTML tags in existing code.. as long as we're not passing user-generated data to these methods, this should be OK (although it would be nice to sanitize the data going into these attributes just in case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writer.Write("\"");
}

return new HtmlString(tag.ToString());
#else
var tag = new TagBuilder("script");
tag.InnerHtml.AppendHtml(script);
writer.Write(">");

if (Environment.Configuration.ScriptNonceProvider != null)
{
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
}
bodyWriter(writer);

return tag;
#endif
}

// In ASP.NET Core, you can no longer call `.ToString` on `IHtmlString`
private static string RenderToString(IHtmlString source)
{
#if LEGACYASPNET
return source.ToString();
#else
using (var writer = new StringWriter())
{
source.WriteTo(writer, HtmlEncoder.Default);
return writer.ToString();
}
#endif
writer.Write("</script>");
}
}
}
32 changes: 0 additions & 32 deletions src/React.Core/GuidExtensions.cs

This file was deleted.

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 @@ -100,6 +102,15 @@ public interface IReactEnvironment
/// <returns>JavaScript for all components</returns>
string GetInitJavaScript(bool clientOnly = false);

/// <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);

/// <summary>
/// Gets the JSX Transformer for this environment.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/React.Core/IReactSiteConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,16 @@ public interface IReactSiteConfiguration
/// <param name="provider"></param>
/// <returns></returns>
IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider);

/// <summary>
/// Get or set boolean that specifies whether the component should be checked to ensure that it exists.
/// </summary>
bool ComponentExistsChecks { get; set; }

/// <summary>
/// Sets whether the component should be checked to ensure that it exists.
/// </summary>
/// <returns>The configuration, for chaining</returns>
IReactSiteConfiguration SetComponentExistsChecks(bool componentExistsChecks);
}
}
Loading