【问题标题】:How to optimise templates for Flask's render_template()如何优化 Flask render_template() 的模板
【发布时间】:2018-12-21 10:20:13
【问题描述】:

我正在为我们的学生酒吧创建一个网络应用程序,以使用 Flask 和 MySQL 数据库来管理交易、库存、客户等。然而,在 Bootstrap 卡片中列出了多达 12 个客户的索引页面(带有支付按钮、充值帐户等),使用 render_template() 渲染需要 4 秒。

此应用部署在具有 2 个 vCPU 的 DigitalOcean droplet 上,并使用典型的 Nginx/Gunicorn duo 在给定时间处理大约 50-100 个并发客户端。我尝试在本地计算机上运行它并得到相同的结果:render_template() 需要很长时间才能呈现页面。

这是模板中需要 4 秒才能渲染的部分:

<div class="container">
  <div class="row">
    {% for user in users.items %}
    <div class="col-lg-3 col-md-4 col-sm-6 col-6">
      <div class="card mb-4 shadow {% if not user.deposit %}text-secondary border-secondary{% elif user.balance <= 0 %}text-danger border-danger{% elif user.balance <= 5 %}text-warning border-warning{% else %}text-primary border-primary{% endif %}">
        <a href="{{ url_for('main.user', username=user.username) }}">
          <img class="card-img-top img-fluid" src="{{ user.avatar() }}" alt="{{ user.username }}">
        </a>
        <div class="card-body">
          <h5 class="card-title text-nowrap user-card-title">
            {% if user.nickname %}
            "{{ user.nickname }}"<br>{{ user.last_name|upper }}
            {% else %}
            {{ user.first_name }}<br>{{ user.last_name|upper }}
            {% endif %}
          </h5>
        </div>
        <div class="card-footer">
          {% if user.deposit %}
          <div class="btn-toolbar justify-content-between" role="toolbar" aria-label="Pay and quick access item">
            <div class="btn-group" role="group" aria-label="Pay">
              {% include '_pay.html.j2' %}
            </div>
            <div class="btn-group" role="group" aria-label="Quick access item">
              {% include '_quick_access_item.html.j2' %}
            </div>
          </div>
          {% else %}
          <div class="btn-toolbar justify-content-between" role="toolbar" aria-label="Deposit">
            <div class="btn-group" role="group" aria-label="Deposit">
              {% include '_deposit.html.j2' %}
            </div>
          </div>
          {% endif %}
        </div>
      </div>
    </div>
    {% endfor %}
  </div>
</div>

“快速访问项目”、“充值”和“存款”是简单的下拉按钮,“支付”是一个可滚动的下拉菜单,最多可以列出 50 种产品。这个索引页面是分页的,用户是一个 Flask 分页对象,每页最多 12 个用户,总共 100 个用户。

这是索引页面的路由函数:

@bp.route('/', methods=['GET'])
@bp.route('/index', methods=['GET'])
@login_required
def index():
    """ View index page. For bartenders, it's the customers page and for clients,
    it redirects to the profile. """
    if not current_user.is_bartender:
        return redirect(url_for('main.user', username=current_user.username))

    # Get arguments
    page = request.args.get('page', 1, type=int)
    sort = request.args.get('sort', 'asc', type=str)
    grad_class = request.args.get('grad_class', str(current_app.config['CURRENT_GRAD_CLASS']), type=int)

    # Get graduating classes
    grad_classes_query = db.session.query(User.grad_class.distinct().label('grad_class'))
    grad_classes = [row.grad_class for row in grad_classes_query.all()]

    # Get inventory
    inventory = Item.query.order_by(Item.name.asc()).all()

    # Get favorite items
    favorite_inventory = Item.query.filter_by(is_favorite=True).order_by(Item.name.asc()).all()

    # Get quick access item
    quick_access_item = Item.query.filter_by(id=current_app.config['QUICK_ACCESS_ITEM_ID']).first()

    # Sort users alphabetically
    if sort == 'asc':
        users = User.query.filter_by(grad_class=grad_class).order_by(User.last_name.asc()).paginate(page,
        current_app.config['USERS_PER_PAGE'], True)
    else:
        users = User.query.filter_by(grad_class=grad_class).order_by(User.last_name.desc()).paginate(page,
            current_app.config['USERS_PER_PAGE'], True)

    return render_template('index.html.j2', title='Checkout',
                        users=users, sort=sort, inventory=inventory,
                        favorite_inventory=favorite_inventory,
                        quick_access_item=quick_access_item,
                        grad_class=grad_class, grad_classes=grad_classes)

我对@9​​87654323@ 中的步骤进行了计时:render_template() 耗时约 4 秒,其余的视图函数耗时约 15 毫秒。

我在这里做错了什么?我的模板太复杂了吗?作为一个网络爱好者,我不知道 jinja2 模板可以承受多少滥用。我考虑过将模板预渲染到静态文件中,但是如果 Nginx 提供静态文件,我如何确保安全(只有调酒师帐户可以访问此页面)?

编辑:这是包含在索引模板中的“支付”下拉模板。他是渲染时间过长的罪魁祸首。

<div class="btn-group pay-btn" role="group">
  <button class="user-card-btn btn {% if user.balance <= 0 %}btn-danger{% elif user.balance <= 5 %}btn-warning{% else %}btn-primary{% endif %} dropdown-toggle{% if user.balance <= 0 or not user.deposit %} disabled{% endif %}" type="button" id="dropdownPay" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    <span class="icon" data-feather="shopping-cart"></span><span class="text">Pay</span>
  </button>
  <div class="dropdown-menu scrollable-menu" aria-labelledby="dropdownPay">
    {% if favorite_inventory|length > 1 %}
    <h6 class="dropdown-header">Favorites</h6>
    {% for item in favorite_inventory %}
    <a class="dropdown-item{% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %} disabled{% endif %}" href="{{ url_for('main.pay', username=user.username, item_name=item.name) }}">
      {% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %}
      <strike>
      {% endif %}
      {{ item.name }} ({{ item.price }}€)
      {% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %}
      </strike>
      {% endif %}
    </a>
    {% endfor %}
    <div class="dropdown-divider"></div>
    {% endif %}
    <h6 class="dropdown-header">Products</h6>
    {% for item in inventory %}
    {% if item not in favorite_inventory %}
    <a class="dropdown-item{% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %} disabled{% endif %}" href="{{ url_for('main.pay', username=user.username, item_name=item.name) }}">
      {% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %}
      <strike>
      {% endif %}
      {{ item.name }} ({{ item.price }}€)
      {% if (not user.deposit) or (user.can_buy(item) != True) or (item.is_quantifiable and item.quantity <= 0) %}
      </strike>
      {% endif %}
    </a>
    {% endif %}
    {% endfor %}
  </div>
</div>

【问题讨论】:

  • 确定时间消耗在渲染而不是python的视图函数中? index() 中的步骤你计时了吗?
  • @roganjosh:确实,我忘了提到我对index() 中的步骤进行了计时:render_template() 函数执行大约需要 4 秒,而视图函数的其余部分需要 15 毫秒,我会在我的问题中添加它!
  • 我可以看到一些可以从 jinja 中提取出来的逻辑,例如{% if user.nickname %} 检查但模板中的任何内容对我来说都是 4 秒值得。我实际上不知道如何分析 jinja2 渲染时间,我想知道是否有一种方法可以获得类似于 Flask 调试分析器的逐行细分(据我所知,它只详细查看 Python 代码) .
  • 我的猜测是{% include '_quick_access_item.html.j2' %} 等人正在消耗您的时间。以及其中的一些东西。我在此模板中看不到任何内容。
  • @roganjosh:你是对的!由于它们只是按钮,我什至没有想到它们是问题所在。 “支付”下拉菜单及其所有项目是罪魁祸首。现在我需要寻找一种方法来先渲染它,然后将它包含在 index.html 中。有没有比这个更好的方法来列出产品(该列表在某种程度上取决于用户,因为有些是每个用户禁用的),还是我应该使用 Javascript 来做到这一点?

标签: python html templates flask jinja2


【解决方案1】:

正如我的问题中提到的,我发现罪魁祸首是“支付”下拉菜单,它每页呈现 12 次并且包含许多项目。为了解决这个问题,我在点击时使用 AJAX 填充每个下拉列表,而不是使用 jinja2 渲染它们:

<script>
$(".pay-btn").click(function() {
  var pay_btn = $(this)
  var dropdown = $(pay_btn).find('.dropdown-menu')
  $.post('/get_user_products', {
    username: $(pay_btn).attr('id').replace('-pay-btn', '')
  }).done(function(response) {
    $(dropdown).append(response['html'])
  }).fail(function() {
    $(dropdown).append('Error: Could not contact server.')
  });
});
</script>

我从这条路线获得用户产品:

@bp.route('/get_user_products', methods=['POST'])
@login_required
def get_user_products():
    """ Returns the list of products that a user can buy. """
    if not current_user.is_bartender:
        flash("You don't have the rights to access this page.", 'danger')
        return redirect(url_for('main.index'))

    # Get user
    user = User.query.filter_by(username=request.form['username']).first_or_404()

    # Get inventory
    inventory = Item.query.order_by(Item.name.asc()).all()

    # Get favorite items
    favorite_inventory = Item.query.filter_by(is_favorite=True).order_by(Item.name.asc()).all()

    pay_template = render_template('_user_products.html.j2', user=user,
                                    inventory=inventory,
                                    favorite_inventory=favorite_inventory)

    return jsonify({'html': pay_template})

因此,我只在实际使用这些下拉菜单并且索引页面的渲染时间恢复正常时才渲染它们。

如果您发现此解决方案不理想,请随时发表评论!

【讨论】:

    猜你喜欢
    • 2020-11-15
    • 2020-06-26
    • 1970-01-01
    • 1970-01-01
    • 2021-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多